Merge remote-tracking branch 'origin/master' into dean-fixes
This commit is contained in:
commit
94c7f50f6e
|
@ -4,8 +4,9 @@ import { Ctx } from "@budibase/types"
|
|||
function validate(
|
||||
schema: Joi.ObjectSchema | Joi.ArraySchema,
|
||||
property: string,
|
||||
opts: { errorPrefix: string } = { errorPrefix: `Invalid ${property}` }
|
||||
opts?: { errorPrefix?: string; allowUnknown?: boolean }
|
||||
) {
|
||||
const errorPrefix = opts?.errorPrefix ?? `Invalid ${property}`
|
||||
// Return a Koa middleware function
|
||||
return (ctx: Ctx, next: any) => {
|
||||
if (!schema) {
|
||||
|
@ -28,10 +29,12 @@ function validate(
|
|||
})
|
||||
}
|
||||
|
||||
const { error } = schema.validate(params)
|
||||
const { error } = schema.validate(params, {
|
||||
allowUnknown: opts?.allowUnknown,
|
||||
})
|
||||
if (error) {
|
||||
let message = error.message
|
||||
if (opts.errorPrefix) {
|
||||
if (errorPrefix) {
|
||||
message = `Invalid ${property} - ${message}`
|
||||
}
|
||||
ctx.throw(400, message)
|
||||
|
@ -42,7 +45,7 @@ function validate(
|
|||
|
||||
export function body(
|
||||
schema: Joi.ObjectSchema | Joi.ArraySchema,
|
||||
opts?: { errorPrefix: string }
|
||||
opts?: { errorPrefix?: string; allowUnknown?: boolean }
|
||||
) {
|
||||
return validate(schema, "body", opts)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { Select, Label, Checkbox, Input, Body } from "@budibase/bbui"
|
||||
import { Select, Label, Checkbox, Body } from "@budibase/bbui"
|
||||
import { tables, viewsV2 } from "stores/builder"
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
|
||||
|
@ -46,19 +46,35 @@
|
|||
|
||||
{#if parameters.confirm}
|
||||
<Label small>Title</Label>
|
||||
<Input placeholder="Delete Row" bind:value={parameters.customTitleText} />
|
||||
<DrawerBindableInput
|
||||
placeholder="Prompt User"
|
||||
value={parameters.customTitleText}
|
||||
on:change={e => (parameters.customTitleText = e.detail)}
|
||||
{bindings}
|
||||
/>
|
||||
|
||||
<Label small>Text</Label>
|
||||
<Input
|
||||
placeholder="Are you sure you want to delete?"
|
||||
bind:value={parameters.confirmText}
|
||||
<DrawerBindableInput
|
||||
placeholder="Are you sure you want to continue?"
|
||||
value={parameters.confirmText}
|
||||
on:change={e => (parameters.confirmText = e.detail)}
|
||||
{bindings}
|
||||
/>
|
||||
|
||||
<Label small>Confirm Text</Label>
|
||||
<Input placeholder="Confirm" bind:value={parameters.confirmButtonText} />
|
||||
|
||||
<DrawerBindableInput
|
||||
placeholder="Confirm"
|
||||
value={parameters.confirmButtonText}
|
||||
on:change={e => (parameters.confirmButtonText = e.detail)}
|
||||
{bindings}
|
||||
/>
|
||||
<Label small>Cancel Text</Label>
|
||||
<Input placeholder="Cancel" bind:value={parameters.cancelButtonText} />
|
||||
<DrawerBindableInput
|
||||
placeholder="Cancel"
|
||||
value={parameters.cancelButtonText}
|
||||
on:change={e => (parameters.cancelButtonText = e.detail)}
|
||||
{bindings}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
<script>
|
||||
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
|
||||
import { Select, Label, Body, Checkbox } from "@budibase/bbui"
|
||||
import {
|
||||
selectedScreen,
|
||||
componentStore,
|
||||
tables,
|
||||
viewsV2,
|
||||
} from "stores/builder"
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
import { getSchemaForDatasourcePlus } from "dataBinding"
|
||||
import SaveFields from "./SaveFields.svelte"
|
||||
import { getDatasourceLikeProviders } from "components/design/settings/controls/ButtonActionEditor/actions/utils"
|
||||
|
@ -73,22 +74,35 @@
|
|||
|
||||
{#if parameters.confirm}
|
||||
<Label small>Title</Label>
|
||||
<Input
|
||||
placeholder="Duplicate Row"
|
||||
bind:value={parameters.customTitleText}
|
||||
<DrawerBindableInput
|
||||
placeholder="Prompt User"
|
||||
value={parameters.customTitleText}
|
||||
on:change={e => (parameters.customTitleText = e.detail)}
|
||||
{bindings}
|
||||
/>
|
||||
|
||||
<Label small>Text</Label>
|
||||
<Input
|
||||
placeholder="Are you sure you want to duplicate this row?"
|
||||
bind:value={parameters.confirmText}
|
||||
<DrawerBindableInput
|
||||
placeholder="Are you sure you want to continue?"
|
||||
value={parameters.confirmText}
|
||||
on:change={e => (parameters.confirmText = e.detail)}
|
||||
{bindings}
|
||||
/>
|
||||
|
||||
<Label small>Confirm Text</Label>
|
||||
<Input placeholder="Confirm" bind:value={parameters.confirmButtonText} />
|
||||
|
||||
<DrawerBindableInput
|
||||
placeholder="Confirm"
|
||||
value={parameters.confirmButtonText}
|
||||
on:change={e => (parameters.confirmButtonText = e.detail)}
|
||||
{bindings}
|
||||
/>
|
||||
<Label small>Cancel Text</Label>
|
||||
<Input placeholder="Cancel" bind:value={parameters.cancelButtonText} />
|
||||
<DrawerBindableInput
|
||||
placeholder="Cancel"
|
||||
value={parameters.cancelButtonText}
|
||||
on:change={e => (parameters.cancelButtonText = e.detail)}
|
||||
{bindings}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { Select, Layout, Input, Checkbox } from "@budibase/bbui"
|
||||
import { Select, Layout, Checkbox } from "@budibase/bbui"
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
import { datasources, integrations, queries } from "stores/builder"
|
||||
import BindingBuilder from "components/integration/QueryBindingBuilder.svelte"
|
||||
import IntegrationQueryEditor from "components/integration/index.svelte"
|
||||
|
@ -58,37 +59,46 @@
|
|||
text="Do not display default notification"
|
||||
bind:value={parameters.notificationOverride}
|
||||
/>
|
||||
<br />
|
||||
{#if parameters.queryId}
|
||||
<Checkbox text="Require confirmation" bind:value={parameters.confirm} />
|
||||
|
||||
{#if parameters.confirm}
|
||||
<Input
|
||||
label="Title"
|
||||
placeholder="Execute Query"
|
||||
bind:value={parameters.customTitleText}
|
||||
/>
|
||||
<div class="params">
|
||||
<DrawerBindableInput
|
||||
label="Title"
|
||||
placeholder="Prompt User"
|
||||
value={parameters.customTitleText}
|
||||
on:change={e => (parameters.customTitleText = e.detail)}
|
||||
{bindings}
|
||||
/>
|
||||
<DrawerBindableInput
|
||||
label="Message"
|
||||
placeholder="Are you sure you want to continue?"
|
||||
value={parameters.confirmText}
|
||||
on:change={e => (parameters.confirmText = e.detail)}
|
||||
{bindings}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Text"
|
||||
placeholder="Are you sure you want to execute this query?"
|
||||
bind:value={parameters.confirmText}
|
||||
/>
|
||||
<DrawerBindableInput
|
||||
label="Confirm Text"
|
||||
placeholder="Confirm"
|
||||
value={parameters.confirmButtonText}
|
||||
on:change={e => (parameters.confirmButtonText = e.detail)}
|
||||
{bindings}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Confirm Text"
|
||||
placeholder="Confirm"
|
||||
bind:value={parameters.confirmButtonText}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Cancel Text"
|
||||
placeholder="Cancel"
|
||||
bind:value={parameters.cancelButtonText}
|
||||
/>
|
||||
<DrawerBindableInput
|
||||
label="Cancel Text"
|
||||
placeholder="Cancel"
|
||||
value={parameters.cancelButtonText}
|
||||
on:change={e => (parameters.cancelButtonText = e.detail)}
|
||||
{bindings}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if query?.parameters?.length > 0}
|
||||
<br />
|
||||
<div class="params">
|
||||
<BindingBuilder
|
||||
customParams={parameters.queryParams}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
<script>
|
||||
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
|
||||
import { Select, Label, Body, Checkbox } from "@budibase/bbui"
|
||||
import {
|
||||
selectedScreen,
|
||||
componentStore,
|
||||
tables,
|
||||
viewsV2,
|
||||
} from "stores/builder"
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
import { getSchemaForDatasourcePlus } from "dataBinding"
|
||||
import SaveFields from "./SaveFields.svelte"
|
||||
import { getDatasourceLikeProviders } from "components/design/settings/controls/ButtonActionEditor/actions/utils"
|
||||
|
@ -73,19 +74,35 @@
|
|||
|
||||
{#if parameters.confirm}
|
||||
<Label small>Title</Label>
|
||||
<Input placeholder="Save Row" bind:value={parameters.customTitleText} />
|
||||
<DrawerBindableInput
|
||||
placeholder="Prompt User"
|
||||
value={parameters.customTitleText}
|
||||
on:change={e => (parameters.customTitleText = e.detail)}
|
||||
{bindings}
|
||||
/>
|
||||
|
||||
<Label small>Text</Label>
|
||||
<Input
|
||||
placeholder="Are you sure you want to save this row?"
|
||||
bind:value={parameters.confirmText}
|
||||
<DrawerBindableInput
|
||||
placeholder="Are you sure you want to continue?"
|
||||
value={parameters.confirmText}
|
||||
on:change={e => (parameters.confirmText = e.detail)}
|
||||
{bindings}
|
||||
/>
|
||||
|
||||
<Label small>Confirm Text</Label>
|
||||
<Input placeholder="Confirm" bind:value={parameters.confirmButtonText} />
|
||||
|
||||
<DrawerBindableInput
|
||||
placeholder="Confirm"
|
||||
value={parameters.confirmButtonText}
|
||||
on:change={e => (parameters.confirmButtonText = e.detail)}
|
||||
{bindings}
|
||||
/>
|
||||
<Label small>Cancel Text</Label>
|
||||
<Input placeholder="Cancel" bind:value={parameters.cancelButtonText} />
|
||||
<DrawerBindableInput
|
||||
placeholder="Cancel"
|
||||
value={parameters.cancelButtonText}
|
||||
on:change={e => (parameters.cancelButtonText = e.detail)}
|
||||
{bindings}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -124,7 +124,7 @@
|
|||
<PropertyControl
|
||||
label="Text align"
|
||||
control={BarButtonList}
|
||||
onChange={align => nav.syncAppNavigation({ textAlign: align })}
|
||||
onChange={align => update("textAlign", align)}
|
||||
value={$nav.textAlign}
|
||||
props={{
|
||||
options: alignmentOptions,
|
||||
|
|
|
@ -1,16 +1,6 @@
|
|||
import * as triggers from "../../automations/triggers"
|
||||
import {
|
||||
getAutomationParams,
|
||||
generateAutomationID,
|
||||
DocumentType,
|
||||
} from "../../db/utils"
|
||||
import {
|
||||
checkForWebhooks,
|
||||
updateTestHistory,
|
||||
removeDeprecated,
|
||||
} from "../../automations/utils"
|
||||
import { deleteEntityMetadata } from "../../utilities"
|
||||
import { MetadataTypes } from "../../constants"
|
||||
import { DocumentType } from "../../db/utils"
|
||||
import { updateTestHistory, removeDeprecated } from "../../automations/utils"
|
||||
import { setTestFlag, clearTestFlag } from "../../utilities/redis"
|
||||
import { context, cache, events, db as dbCore } from "@budibase/backend-core"
|
||||
import { automations, features } from "@budibase/pro"
|
||||
|
@ -41,42 +31,9 @@ function getTriggerDefinitions() {
|
|||
* *
|
||||
*************************/
|
||||
|
||||
async function cleanupAutomationMetadata(automationId: string) {
|
||||
await deleteEntityMetadata(MetadataTypes.AUTOMATION_TEST_INPUT, automationId)
|
||||
await deleteEntityMetadata(
|
||||
MetadataTypes.AUTOMATION_TEST_HISTORY,
|
||||
automationId
|
||||
)
|
||||
}
|
||||
|
||||
function cleanAutomationInputs(automation: Automation) {
|
||||
if (automation == null) {
|
||||
return automation
|
||||
}
|
||||
let steps = automation.definition.steps
|
||||
let trigger = automation.definition.trigger
|
||||
let allSteps = [...steps, trigger]
|
||||
// live is not a property used anymore
|
||||
if (automation.live != null) {
|
||||
delete automation.live
|
||||
}
|
||||
for (let step of allSteps) {
|
||||
if (step == null) {
|
||||
continue
|
||||
}
|
||||
for (let inputName of Object.keys(step.inputs)) {
|
||||
if (!step.inputs[inputName] || step.inputs[inputName] === "") {
|
||||
delete step.inputs[inputName]
|
||||
}
|
||||
}
|
||||
}
|
||||
return automation
|
||||
}
|
||||
|
||||
export async function create(
|
||||
ctx: UserCtx<Automation, { message: string; automation: Automation }>
|
||||
) {
|
||||
const db = context.getAppDB()
|
||||
let automation = ctx.request.body
|
||||
automation.appId = ctx.appId
|
||||
|
||||
|
@ -86,66 +43,17 @@ export async function create(
|
|||
return
|
||||
}
|
||||
|
||||
// Respect existing IDs if recreating a deleted automation
|
||||
if (!automation._id) {
|
||||
automation._id = generateAutomationID()
|
||||
}
|
||||
|
||||
automation.type = "automation"
|
||||
automation = cleanAutomationInputs(automation)
|
||||
automation = await checkForWebhooks({
|
||||
newAuto: automation,
|
||||
})
|
||||
const response = await db.put(automation)
|
||||
await events.automation.created(automation)
|
||||
for (let step of automation.definition.steps) {
|
||||
await events.automation.stepCreated(automation, step)
|
||||
}
|
||||
automation._rev = response.rev
|
||||
const createdAutomation = await sdk.automations.create(automation)
|
||||
|
||||
ctx.status = 200
|
||||
ctx.body = {
|
||||
message: "Automation created successfully",
|
||||
automation: {
|
||||
...automation,
|
||||
...response,
|
||||
},
|
||||
automation: createdAutomation,
|
||||
}
|
||||
builderSocket?.emitAutomationUpdate(ctx, automation)
|
||||
}
|
||||
|
||||
export function getNewSteps(oldAutomation: Automation, automation: Automation) {
|
||||
const oldStepIds = oldAutomation.definition.steps.map(s => s.id)
|
||||
return automation.definition.steps.filter(s => !oldStepIds.includes(s.id))
|
||||
}
|
||||
|
||||
export function getDeletedSteps(
|
||||
oldAutomation: Automation,
|
||||
automation: Automation
|
||||
) {
|
||||
const stepIds = automation.definition.steps.map(s => s.id)
|
||||
return oldAutomation.definition.steps.filter(s => !stepIds.includes(s.id))
|
||||
}
|
||||
|
||||
export async function handleStepEvents(
|
||||
oldAutomation: Automation,
|
||||
automation: Automation
|
||||
) {
|
||||
// new steps
|
||||
const newSteps = getNewSteps(oldAutomation, automation)
|
||||
for (let step of newSteps) {
|
||||
await events.automation.stepCreated(automation, step)
|
||||
}
|
||||
|
||||
// old steps
|
||||
const deletedSteps = getDeletedSteps(oldAutomation, automation)
|
||||
for (let step of deletedSteps) {
|
||||
await events.automation.stepDeleted(automation, step)
|
||||
}
|
||||
}
|
||||
|
||||
export async function update(ctx: UserCtx) {
|
||||
const db = context.getAppDB()
|
||||
let automation = ctx.request.body
|
||||
automation.appId = ctx.appId
|
||||
|
||||
|
@ -155,72 +63,28 @@ export async function update(ctx: UserCtx) {
|
|||
return
|
||||
}
|
||||
|
||||
const oldAutomation = await db.get<Automation>(automation._id)
|
||||
automation = cleanAutomationInputs(automation)
|
||||
automation = await checkForWebhooks({
|
||||
oldAuto: oldAutomation,
|
||||
newAuto: automation,
|
||||
})
|
||||
const response = await db.put(automation)
|
||||
automation._rev = response.rev
|
||||
|
||||
const oldAutoTrigger =
|
||||
oldAutomation && oldAutomation.definition.trigger
|
||||
? oldAutomation.definition.trigger
|
||||
: undefined
|
||||
const newAutoTrigger =
|
||||
automation && automation.definition.trigger
|
||||
? automation.definition.trigger
|
||||
: {}
|
||||
// trigger has been updated, remove the test inputs
|
||||
if (oldAutoTrigger && oldAutoTrigger.id !== newAutoTrigger.id) {
|
||||
await events.automation.triggerUpdated(automation)
|
||||
await deleteEntityMetadata(
|
||||
MetadataTypes.AUTOMATION_TEST_INPUT,
|
||||
automation._id!
|
||||
)
|
||||
}
|
||||
|
||||
await handleStepEvents(oldAutomation, automation)
|
||||
const updatedAutomation = await sdk.automations.update(automation)
|
||||
|
||||
ctx.status = 200
|
||||
ctx.body = {
|
||||
message: `Automation ${automation._id} updated successfully.`,
|
||||
automation: {
|
||||
...automation,
|
||||
_rev: response.rev,
|
||||
_id: response.id,
|
||||
},
|
||||
automation: updatedAutomation,
|
||||
}
|
||||
builderSocket?.emitAutomationUpdate(ctx, automation)
|
||||
}
|
||||
|
||||
export async function fetch(ctx: UserCtx) {
|
||||
const db = context.getAppDB()
|
||||
const response = await db.allDocs(
|
||||
getAutomationParams(null, {
|
||||
include_docs: true,
|
||||
})
|
||||
)
|
||||
ctx.body = response.rows.map(row => row.doc)
|
||||
ctx.body = await sdk.automations.fetch()
|
||||
}
|
||||
|
||||
export async function find(ctx: UserCtx) {
|
||||
const db = context.getAppDB()
|
||||
ctx.body = await db.get(ctx.params.id)
|
||||
ctx.body = await sdk.automations.get(ctx.params.id)
|
||||
}
|
||||
|
||||
export async function destroy(ctx: UserCtx<void, DeleteAutomationResponse>) {
|
||||
const db = context.getAppDB()
|
||||
const automationId = ctx.params.id
|
||||
const oldAutomation = await db.get<Automation>(automationId)
|
||||
await checkForWebhooks({
|
||||
oldAuto: oldAutomation,
|
||||
})
|
||||
// delete metadata first
|
||||
await cleanupAutomationMetadata(automationId)
|
||||
ctx.body = await db.remove(automationId, ctx.params.rev)
|
||||
await events.automation.deleted(oldAutomation)
|
||||
|
||||
ctx.body = await sdk.automations.remove(automationId, ctx.params.rev)
|
||||
builderSocket?.emitAutomationDeletion(ctx, automationId)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
import {
|
||||
CreateRowActionRequest,
|
||||
Ctx,
|
||||
RowActionResponse,
|
||||
RowActionsResponse,
|
||||
UpdateRowActionRequest,
|
||||
} from "@budibase/types"
|
||||
import sdk from "../../../sdk"
|
||||
|
||||
async function getTable(ctx: Ctx) {
|
||||
const { tableId } = ctx.params
|
||||
const table = await sdk.tables.getTable(tableId)
|
||||
if (!table) {
|
||||
ctx.throw(404)
|
||||
}
|
||||
return table
|
||||
}
|
||||
|
||||
export async function find(ctx: Ctx<void, RowActionsResponse>) {
|
||||
const table = await getTable(ctx)
|
||||
|
||||
if (!(await sdk.rowActions.docExists(table._id!))) {
|
||||
ctx.body = {
|
||||
actions: {},
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const { actions } = await sdk.rowActions.get(table._id!)
|
||||
const result: RowActionsResponse = {
|
||||
actions: Object.entries(actions).reduce<Record<string, RowActionResponse>>(
|
||||
(acc, [key, action]) => ({
|
||||
...acc,
|
||||
[key]: { id: key, tableId: table._id!, ...action },
|
||||
}),
|
||||
{}
|
||||
),
|
||||
}
|
||||
ctx.body = result
|
||||
}
|
||||
|
||||
export async function create(
|
||||
ctx: Ctx<CreateRowActionRequest, RowActionResponse>
|
||||
) {
|
||||
const table = await getTable(ctx)
|
||||
|
||||
const createdAction = await sdk.rowActions.create(table._id!, {
|
||||
name: ctx.request.body.name,
|
||||
})
|
||||
|
||||
ctx.body = {
|
||||
tableId: table._id!,
|
||||
...createdAction,
|
||||
}
|
||||
ctx.status = 201
|
||||
}
|
||||
|
||||
export async function update(
|
||||
ctx: Ctx<UpdateRowActionRequest, RowActionResponse>
|
||||
) {
|
||||
const table = await getTable(ctx)
|
||||
const { actionId } = ctx.params
|
||||
|
||||
const actions = await sdk.rowActions.update(table._id!, actionId, {
|
||||
name: ctx.request.body.name,
|
||||
})
|
||||
|
||||
ctx.body = {
|
||||
tableId: table._id!,
|
||||
...actions,
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove(ctx: Ctx<void, void>) {
|
||||
const table = await getTable(ctx)
|
||||
const { actionId } = ctx.params
|
||||
|
||||
await sdk.rowActions.remove(table._id!, actionId)
|
||||
ctx.status = 204
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./crud"
|
||||
export * from "./run"
|
|
@ -0,0 +1,3 @@
|
|||
export function run() {
|
||||
throw new Error("Function not implemented.")
|
||||
}
|
|
@ -28,6 +28,7 @@ import opsRoutes from "./ops"
|
|||
import debugRoutes from "./debug"
|
||||
import Router from "@koa/router"
|
||||
import { api as pro } from "@budibase/pro"
|
||||
import rowActionRoutes from "./rowAction"
|
||||
|
||||
export { default as staticRoutes } from "./static"
|
||||
export { default as publicRoutes } from "./public"
|
||||
|
@ -65,6 +66,7 @@ export const mainRoutes: Router[] = [
|
|||
opsRoutes,
|
||||
debugRoutes,
|
||||
environmentVariableRoutes,
|
||||
rowActionRoutes,
|
||||
// these need to be handled last as they still use /api/:tableId
|
||||
// this could be breaking as koa may recognise other routes as this
|
||||
tableRoutes,
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import Router from "@koa/router"
|
||||
import * as rowActionController from "../controllers/rowAction"
|
||||
import { authorizedResource } from "../../middleware/authorized"
|
||||
|
||||
import { middleware, permissions } from "@budibase/backend-core"
|
||||
import Joi from "joi"
|
||||
|
||||
const { PermissionLevel, PermissionType } = permissions
|
||||
|
||||
export function rowActionValidator() {
|
||||
return middleware.joiValidator.body(
|
||||
Joi.object({
|
||||
name: Joi.string().required(),
|
||||
}),
|
||||
{ allowUnknown: true }
|
||||
)
|
||||
}
|
||||
|
||||
const router: Router = new Router()
|
||||
|
||||
// CRUD endpoints
|
||||
router
|
||||
.get(
|
||||
"/api/tables/:tableId/actions",
|
||||
authorizedResource(PermissionType.TABLE, PermissionLevel.READ, "tableId"),
|
||||
rowActionController.find
|
||||
)
|
||||
.post(
|
||||
"/api/tables/:tableId/actions",
|
||||
authorizedResource(PermissionType.TABLE, PermissionLevel.READ, "tableId"),
|
||||
rowActionValidator(),
|
||||
rowActionController.create
|
||||
)
|
||||
.put(
|
||||
"/api/tables/:tableId/actions/:actionId",
|
||||
authorizedResource(PermissionType.TABLE, PermissionLevel.READ, "tableId"),
|
||||
rowActionValidator(),
|
||||
rowActionController.update
|
||||
)
|
||||
.delete(
|
||||
"/api/tables/:tableId/actions/:actionId",
|
||||
authorizedResource(PermissionType.TABLE, PermissionLevel.READ, "tableId"),
|
||||
rowActionController.remove
|
||||
)
|
||||
|
||||
// Other endpoints
|
||||
.post(
|
||||
"/api/tables/:tableId/actions/:actionId/run",
|
||||
authorizedResource(PermissionType.TABLE, PermissionLevel.READ, "tableId"),
|
||||
rowActionController.run
|
||||
)
|
||||
|
||||
export default router
|
|
@ -154,7 +154,7 @@ describe("/automations", () => {
|
|||
tableId: table._id,
|
||||
},
|
||||
}
|
||||
automation.appId = config.appId
|
||||
automation.appId = config.getAppId()
|
||||
automation = await config.createAutomation(automation)
|
||||
await setup.delay(500)
|
||||
const res = await testAutomation(config, automation, {
|
||||
|
@ -267,8 +267,7 @@ describe("/automations", () => {
|
|||
}
|
||||
|
||||
it("updates a automations name", async () => {
|
||||
let automation = newAutomation()
|
||||
await config.createAutomation(automation)
|
||||
const automation = await config.createAutomation(newAutomation())
|
||||
automation.name = "Updated Name"
|
||||
jest.clearAllMocks()
|
||||
|
||||
|
@ -294,8 +293,7 @@ describe("/automations", () => {
|
|||
})
|
||||
|
||||
it("updates a automations name using POST request", async () => {
|
||||
let automation = newAutomation()
|
||||
await config.createAutomation(automation)
|
||||
const automation = await config.createAutomation(newAutomation())
|
||||
automation.name = "Updated Name"
|
||||
jest.clearAllMocks()
|
||||
|
||||
|
@ -392,8 +390,7 @@ describe("/automations", () => {
|
|||
describe("fetch", () => {
|
||||
it("return all the automations for an instance", async () => {
|
||||
await clearAllAutomations(config)
|
||||
const autoConfig = basicAutomation()
|
||||
await config.createAutomation(autoConfig)
|
||||
const autoConfig = await config.createAutomation(basicAutomation())
|
||||
const res = await request
|
||||
.get(`/api/automations`)
|
||||
.set(config.defaultHeaders())
|
||||
|
|
|
@ -0,0 +1,412 @@
|
|||
import _ from "lodash"
|
||||
import tk from "timekeeper"
|
||||
|
||||
import { CreateRowActionRequest, RowActionResponse } from "@budibase/types"
|
||||
import * as setup from "./utilities"
|
||||
import { generator } from "@budibase/backend-core/tests"
|
||||
|
||||
describe("/rowsActions", () => {
|
||||
const config = setup.getConfig()
|
||||
|
||||
let tableId: string
|
||||
|
||||
beforeAll(async () => {
|
||||
tk.freeze(new Date())
|
||||
await config.init()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
const table = await config.api.table.save(setup.structures.basicTable())
|
||||
tableId = table._id!
|
||||
})
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
|
||||
const createRowAction = config.api.rowAction.save
|
||||
|
||||
function createRowActionRequest(): CreateRowActionRequest {
|
||||
return {
|
||||
name: generator.string(),
|
||||
}
|
||||
}
|
||||
|
||||
function createRowActionRequests(count: number): CreateRowActionRequest[] {
|
||||
return generator
|
||||
.unique(() => generator.string(), count)
|
||||
.map(name => ({ name }))
|
||||
}
|
||||
|
||||
function unauthorisedTests() {
|
||||
it("returns unauthorised (401) for unauthenticated requests", async () => {
|
||||
await createRowAction(
|
||||
tableId,
|
||||
createRowActionRequest(),
|
||||
{
|
||||
status: 401,
|
||||
body: {
|
||||
message: "Session not authenticated",
|
||||
},
|
||||
},
|
||||
{ publicUser: true }
|
||||
)
|
||||
})
|
||||
|
||||
it("returns forbidden (403) for non-builder users", async () => {
|
||||
const user = await config.createUser({
|
||||
builder: {},
|
||||
})
|
||||
await config.withUser(user, async () => {
|
||||
await createRowAction(generator.guid(), createRowActionRequest(), {
|
||||
status: 403,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("rejects (404) for a non-existing table", async () => {
|
||||
await createRowAction(generator.guid(), createRowActionRequest(), {
|
||||
status: 404,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
describe("create", () => {
|
||||
unauthorisedTests()
|
||||
|
||||
it("creates new row actions for tables without existing actions", async () => {
|
||||
const rowAction = createRowActionRequest()
|
||||
const res = await createRowAction(tableId, rowAction, {
|
||||
status: 201,
|
||||
})
|
||||
|
||||
expect(res).toEqual({
|
||||
id: expect.stringMatching(/^row_action_\w+/),
|
||||
tableId: tableId,
|
||||
...rowAction,
|
||||
})
|
||||
|
||||
expect(await config.api.rowAction.find(tableId)).toEqual({
|
||||
actions: {
|
||||
[res.id]: {
|
||||
...rowAction,
|
||||
id: res.id,
|
||||
tableId: tableId,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("trims row action names", async () => {
|
||||
const name = " action name "
|
||||
const res = await createRowAction(
|
||||
tableId,
|
||||
{ name },
|
||||
{
|
||||
status: 201,
|
||||
}
|
||||
)
|
||||
|
||||
expect(res).toEqual({
|
||||
id: expect.stringMatching(/^row_action_\w+/),
|
||||
tableId: tableId,
|
||||
name: "action name",
|
||||
})
|
||||
|
||||
expect(await config.api.rowAction.find(tableId)).toEqual({
|
||||
actions: {
|
||||
[res.id]: expect.objectContaining({
|
||||
name: "action name",
|
||||
}),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("can create multiple row actions for the same table", async () => {
|
||||
const rowActions = createRowActionRequests(3)
|
||||
const responses: RowActionResponse[] = []
|
||||
for (const action of rowActions) {
|
||||
responses.push(await createRowAction(tableId, action))
|
||||
}
|
||||
|
||||
expect(await config.api.rowAction.find(tableId)).toEqual({
|
||||
actions: {
|
||||
[responses[0].id]: { ...rowActions[0], id: responses[0].id, tableId },
|
||||
[responses[1].id]: { ...rowActions[1], id: responses[1].id, tableId },
|
||||
[responses[2].id]: { ...rowActions[2], id: responses[2].id, tableId },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("rejects with bad request when creating with no name", async () => {
|
||||
const rowAction: CreateRowActionRequest = {
|
||||
name: "",
|
||||
}
|
||||
|
||||
await createRowAction(tableId, rowAction, {
|
||||
status: 400,
|
||||
body: {
|
||||
message: 'Invalid body - "name" is not allowed to be empty',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("ignores not valid row action data", async () => {
|
||||
const rowAction = createRowActionRequest()
|
||||
const dirtyRowAction = {
|
||||
...rowAction,
|
||||
id: generator.guid(),
|
||||
valueToIgnore: generator.string(),
|
||||
}
|
||||
const res = await createRowAction(tableId, dirtyRowAction, {
|
||||
status: 201,
|
||||
})
|
||||
|
||||
expect(res).toEqual({
|
||||
id: expect.any(String),
|
||||
tableId,
|
||||
...rowAction,
|
||||
})
|
||||
|
||||
expect(await config.api.rowAction.find(tableId)).toEqual({
|
||||
actions: {
|
||||
[res.id]: {
|
||||
id: res.id,
|
||||
tableId: tableId,
|
||||
...rowAction,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("can not create multiple row actions with the same name (for the same table)", async () => {
|
||||
const action = await createRowAction(tableId, {
|
||||
name: "Row action name ",
|
||||
})
|
||||
|
||||
await createRowAction(
|
||||
tableId,
|
||||
{ name: action.name },
|
||||
{
|
||||
status: 409,
|
||||
body: {
|
||||
message: "A row action with the same name already exists.",
|
||||
},
|
||||
}
|
||||
)
|
||||
await createRowAction(
|
||||
tableId,
|
||||
{ name: "row action name" },
|
||||
{
|
||||
status: 409,
|
||||
body: {
|
||||
message: "A row action with the same name already exists.",
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("can reuse row action names between different tables", async () => {
|
||||
const otherTable = await config.api.table.save(
|
||||
setup.structures.basicTable()
|
||||
)
|
||||
|
||||
const action = await createRowAction(tableId, createRowActionRequest())
|
||||
|
||||
await createRowAction(otherTable._id!, { name: action.name })
|
||||
})
|
||||
})
|
||||
|
||||
describe("find", () => {
|
||||
unauthorisedTests()
|
||||
|
||||
it("returns only the actions for the requested table", async () => {
|
||||
const rowActions: RowActionResponse[] = []
|
||||
for (const action of createRowActionRequests(3)) {
|
||||
rowActions.push(await createRowAction(tableId, action))
|
||||
}
|
||||
|
||||
const otherTable = await config.api.table.save(
|
||||
setup.structures.basicTable()
|
||||
)
|
||||
await createRowAction(otherTable._id!, createRowActionRequest())
|
||||
|
||||
const response = await config.api.rowAction.find(tableId)
|
||||
expect(response).toEqual({
|
||||
actions: {
|
||||
[rowActions[0].id]: expect.any(Object),
|
||||
[rowActions[1].id]: expect.any(Object),
|
||||
[rowActions[2].id]: expect.any(Object),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("returns empty for tables without row actions", async () => {
|
||||
const response = await config.api.rowAction.find(tableId)
|
||||
expect(response).toEqual({
|
||||
actions: {},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
unauthorisedTests()
|
||||
|
||||
it("can update existing actions", async () => {
|
||||
for (const rowAction of createRowActionRequests(3)) {
|
||||
await createRowAction(tableId, rowAction)
|
||||
}
|
||||
|
||||
const persisted = await config.api.rowAction.find(tableId)
|
||||
|
||||
const [actionId, actionData] = _.sample(
|
||||
Object.entries(persisted.actions)
|
||||
)!
|
||||
|
||||
const updatedName = generator.string()
|
||||
|
||||
const res = await config.api.rowAction.update(tableId, actionId, {
|
||||
...actionData,
|
||||
name: updatedName,
|
||||
})
|
||||
|
||||
expect(res).toEqual({
|
||||
id: actionId,
|
||||
tableId,
|
||||
name: updatedName,
|
||||
})
|
||||
|
||||
expect(await config.api.rowAction.find(tableId)).toEqual(
|
||||
expect.objectContaining({
|
||||
actions: expect.objectContaining({
|
||||
[actionId]: {
|
||||
...actionData,
|
||||
name: updatedName,
|
||||
},
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("trims row action names", async () => {
|
||||
const rowAction = await createRowAction(
|
||||
tableId,
|
||||
createRowActionRequest(),
|
||||
{
|
||||
status: 201,
|
||||
}
|
||||
)
|
||||
|
||||
const res = await config.api.rowAction.update(tableId, rowAction.id, {
|
||||
...rowAction,
|
||||
name: " action name ",
|
||||
})
|
||||
|
||||
expect(res).toEqual(expect.objectContaining({ name: "action name" }))
|
||||
|
||||
expect(await config.api.rowAction.find(tableId)).toEqual(
|
||||
expect.objectContaining({
|
||||
actions: expect.objectContaining({
|
||||
[rowAction.id]: expect.objectContaining({
|
||||
name: "action name",
|
||||
}),
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("throws Bad Request when trying to update by a non-existing id", async () => {
|
||||
await createRowAction(tableId, createRowActionRequest())
|
||||
|
||||
await config.api.rowAction.update(
|
||||
tableId,
|
||||
generator.guid(),
|
||||
createRowActionRequest(),
|
||||
{ status: 400 }
|
||||
)
|
||||
})
|
||||
|
||||
it("throws Bad Request when trying to update by a via another table id", async () => {
|
||||
const otherTable = await config.api.table.save(
|
||||
setup.structures.basicTable()
|
||||
)
|
||||
await createRowAction(otherTable._id!, createRowActionRequest())
|
||||
|
||||
const action = await createRowAction(tableId, createRowActionRequest())
|
||||
await config.api.rowAction.update(
|
||||
otherTable._id!,
|
||||
action.id,
|
||||
createRowActionRequest(),
|
||||
{ status: 400 }
|
||||
)
|
||||
})
|
||||
|
||||
it("can not use existing row action names (for the same table)", async () => {
|
||||
const action1 = await createRowAction(tableId, createRowActionRequest())
|
||||
const action2 = await createRowAction(tableId, createRowActionRequest())
|
||||
|
||||
await config.api.rowAction.update(
|
||||
tableId,
|
||||
action1.id,
|
||||
{ name: action2.name },
|
||||
{
|
||||
status: 409,
|
||||
body: {
|
||||
message: "A row action with the same name already exists.",
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("does not throw with name conflicts for the same row action", async () => {
|
||||
const action1 = await createRowAction(tableId, createRowActionRequest())
|
||||
|
||||
await config.api.rowAction.update(tableId, action1.id, {
|
||||
name: action1.name,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("delete", () => {
|
||||
unauthorisedTests()
|
||||
|
||||
it("can delete existing actions", async () => {
|
||||
const actions: RowActionResponse[] = []
|
||||
for (const rowAction of createRowActionRequests(3)) {
|
||||
actions.push(await createRowAction(tableId, rowAction))
|
||||
}
|
||||
|
||||
const actionToDelete = _.sample(actions)!
|
||||
|
||||
await config.api.rowAction.delete(tableId, actionToDelete.id, {
|
||||
status: 204,
|
||||
})
|
||||
|
||||
expect(await config.api.rowAction.find(tableId)).toEqual(
|
||||
expect.objectContaining({
|
||||
actions: actions
|
||||
.filter(a => a.id !== actionToDelete.id)
|
||||
.reduce((acc, c) => ({ ...acc, [c.id]: expect.any(Object) }), {}),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("throws Bad Request when trying to delete by a non-existing id", async () => {
|
||||
await createRowAction(tableId, createRowActionRequest())
|
||||
|
||||
await config.api.rowAction.delete(tableId, generator.guid(), {
|
||||
status: 400,
|
||||
})
|
||||
})
|
||||
|
||||
it("throws Bad Request when trying to delete by a via another table id", async () => {
|
||||
const otherTable = await config.api.table.save(
|
||||
setup.structures.basicTable()
|
||||
)
|
||||
await createRowAction(otherTable._id!, createRowActionRequest())
|
||||
|
||||
const action = await createRowAction(tableId, createRowActionRequest())
|
||||
await config.api.rowAction.delete(otherTable._id!, action.id, {
|
||||
status: 400,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -4,7 +4,12 @@ import {
|
|||
getDatasource,
|
||||
knexClient,
|
||||
} from "../../../integrations/tests/utils"
|
||||
import { db as dbCore, utils } from "@budibase/backend-core"
|
||||
import {
|
||||
db as dbCore,
|
||||
MAX_VALID_DATE,
|
||||
MIN_VALID_DATE,
|
||||
utils,
|
||||
} from "@budibase/backend-core"
|
||||
|
||||
import * as setup from "./utilities"
|
||||
import {
|
||||
|
@ -1098,21 +1103,37 @@ describe.each([
|
|||
}).toFindNothing()
|
||||
})
|
||||
|
||||
// We never implemented half-open ranges in Lucene.
|
||||
!isLucene &&
|
||||
it("can search using just a low value", async () => {
|
||||
await expectQuery({
|
||||
range: { age: { low: 5 } },
|
||||
}).toContainExactly([{ age: 10 }])
|
||||
})
|
||||
it("greater than equal to", async () => {
|
||||
await expectQuery({
|
||||
range: {
|
||||
age: { low: 10, high: Number.MAX_SAFE_INTEGER },
|
||||
},
|
||||
}).toContainExactly([{ age: 10 }])
|
||||
})
|
||||
|
||||
// We never implemented half-open ranges in Lucene.
|
||||
!isLucene &&
|
||||
it("can search using just a high value", async () => {
|
||||
await expectQuery({
|
||||
range: { age: { high: 5 } },
|
||||
}).toContainExactly([{ age: 1 }])
|
||||
})
|
||||
it("greater than", async () => {
|
||||
await expectQuery({
|
||||
range: {
|
||||
age: { low: 5, high: Number.MAX_SAFE_INTEGER },
|
||||
},
|
||||
}).toContainExactly([{ age: 10 }])
|
||||
})
|
||||
|
||||
it("less than equal to", async () => {
|
||||
await expectQuery({
|
||||
range: {
|
||||
age: { high: 1, low: Number.MIN_SAFE_INTEGER },
|
||||
},
|
||||
}).toContainExactly([{ age: 1 }])
|
||||
})
|
||||
|
||||
it("less than", async () => {
|
||||
await expectQuery({
|
||||
range: {
|
||||
age: { high: 5, low: Number.MIN_SAFE_INTEGER },
|
||||
},
|
||||
}).toContainExactly([{ age: 1 }])
|
||||
})
|
||||
})
|
||||
|
||||
describe("sort", () => {
|
||||
|
@ -1232,21 +1253,29 @@ describe.each([
|
|||
}).toFindNothing()
|
||||
})
|
||||
|
||||
// We never implemented half-open ranges in Lucene.
|
||||
!isLucene &&
|
||||
it("can search using just a low value", async () => {
|
||||
await expectQuery({
|
||||
range: { dob: { low: JAN_5TH } },
|
||||
}).toContainExactly([{ dob: JAN_10TH }])
|
||||
})
|
||||
it("greater than equal to", async () => {
|
||||
await expectQuery({
|
||||
range: { dob: { low: JAN_10TH, high: MAX_VALID_DATE.toISOString() } },
|
||||
}).toContainExactly([{ dob: JAN_10TH }])
|
||||
})
|
||||
|
||||
// We never implemented half-open ranges in Lucene.
|
||||
!isLucene &&
|
||||
it("can search using just a high value", async () => {
|
||||
await expectQuery({
|
||||
range: { dob: { high: JAN_5TH } },
|
||||
}).toContainExactly([{ dob: JAN_1ST }])
|
||||
})
|
||||
it("greater than", async () => {
|
||||
await expectQuery({
|
||||
range: { dob: { low: JAN_5TH, high: MAX_VALID_DATE.toISOString() } },
|
||||
}).toContainExactly([{ dob: JAN_10TH }])
|
||||
})
|
||||
|
||||
it("less than equal to", async () => {
|
||||
await expectQuery({
|
||||
range: { dob: { high: JAN_1ST, low: MIN_VALID_DATE.toISOString() } },
|
||||
}).toContainExactly([{ dob: JAN_1ST }])
|
||||
})
|
||||
|
||||
it("less than", async () => {
|
||||
await expectQuery({
|
||||
range: { dob: { high: JAN_5TH, low: MIN_VALID_DATE.toISOString() } },
|
||||
}).toContainExactly([{ dob: JAN_1ST }])
|
||||
})
|
||||
})
|
||||
|
||||
describe("sort", () => {
|
||||
|
|
|
@ -7,18 +7,11 @@ import { db as dbCore, context, utils } from "@budibase/backend-core"
|
|||
import { getAutomationMetadataParams } from "../db/utils"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { quotas } from "@budibase/pro"
|
||||
import {
|
||||
Automation,
|
||||
AutomationJob,
|
||||
Webhook,
|
||||
WebhookActionType,
|
||||
} from "@budibase/types"
|
||||
import sdk from "../sdk"
|
||||
import { Automation, AutomationJob } from "@budibase/types"
|
||||
import { automationsEnabled } from "../features"
|
||||
import { helpers, REBOOT_CRON } from "@budibase/shared-core"
|
||||
import tracer from "dd-trace"
|
||||
|
||||
const WH_STEP_ID = definitions.WEBHOOK.stepId
|
||||
const CRON_STEP_ID = definitions.CRON.stepId
|
||||
let Runner: Thread
|
||||
if (automationsEnabled()) {
|
||||
|
@ -229,76 +222,6 @@ export async function enableCronTrigger(appId: any, automation: Automation) {
|
|||
return { enabled, automation }
|
||||
}
|
||||
|
||||
/**
|
||||
* This function handles checking if any webhooks need to be created or deleted for automations.
|
||||
* @param appId The ID of the app in which we are checking for webhooks
|
||||
* @param oldAuto The old automation object if updating/deleting
|
||||
* @param newAuto The new automation object if creating/updating
|
||||
* @returns After this is complete the new automation object may have been updated and should be
|
||||
* written to DB (this does not write to DB as it would be wasteful to repeat).
|
||||
*/
|
||||
export async function checkForWebhooks({ oldAuto, newAuto }: any) {
|
||||
const appId = context.getAppId()
|
||||
if (!appId) {
|
||||
throw new Error("Unable to check webhooks - no app ID in context.")
|
||||
}
|
||||
const oldTrigger = oldAuto ? oldAuto.definition.trigger : null
|
||||
const newTrigger = newAuto ? newAuto.definition.trigger : null
|
||||
const triggerChanged =
|
||||
oldTrigger && newTrigger && oldTrigger.id !== newTrigger.id
|
||||
function isWebhookTrigger(auto: any) {
|
||||
return (
|
||||
auto &&
|
||||
auto.definition.trigger &&
|
||||
auto.definition.trigger.stepId === WH_STEP_ID
|
||||
)
|
||||
}
|
||||
// need to delete webhook
|
||||
if (
|
||||
isWebhookTrigger(oldAuto) &&
|
||||
(!isWebhookTrigger(newAuto) || triggerChanged) &&
|
||||
oldTrigger.webhookId
|
||||
) {
|
||||
try {
|
||||
const db = context.getAppDB()
|
||||
// need to get the webhook to get the rev
|
||||
const webhook = await db.get<Webhook>(oldTrigger.webhookId)
|
||||
// might be updating - reset the inputs to remove the URLs
|
||||
if (newTrigger) {
|
||||
delete newTrigger.webhookId
|
||||
newTrigger.inputs = {}
|
||||
}
|
||||
await sdk.automations.webhook.destroy(webhook._id!, webhook._rev!)
|
||||
} catch (err) {
|
||||
// don't worry about not being able to delete, if it doesn't exist all good
|
||||
}
|
||||
}
|
||||
// need to create webhook
|
||||
if (
|
||||
(!isWebhookTrigger(oldAuto) || triggerChanged) &&
|
||||
isWebhookTrigger(newAuto)
|
||||
) {
|
||||
const webhook = await sdk.automations.webhook.save(
|
||||
sdk.automations.webhook.newDoc(
|
||||
"Automation webhook",
|
||||
WebhookActionType.AUTOMATION,
|
||||
newAuto._id
|
||||
)
|
||||
)
|
||||
const id = webhook._id
|
||||
newTrigger.webhookId = id
|
||||
// the app ID has to be development for this endpoint
|
||||
// it can only be used when building the app
|
||||
// but the trigger endpoint will always be used in production
|
||||
const prodAppId = dbCore.getProdAppID(appId)
|
||||
newTrigger.inputs = {
|
||||
schemaUrl: `api/webhooks/schema/${appId}/${id}`,
|
||||
triggerUrl: `api/webhooks/trigger/${prodAppId}/${id}`,
|
||||
}
|
||||
}
|
||||
return newAuto
|
||||
}
|
||||
|
||||
/**
|
||||
* When removing an app/unpublishing it need to make sure automations are cleaned up (cron).
|
||||
* @param appId the app that is being removed.
|
||||
|
|
|
@ -349,3 +349,11 @@ export function isRelationshipColumn(
|
|||
): column is RelationshipFieldMetadata {
|
||||
return column.type === FieldType.LINK
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new row actions ID.
|
||||
* @returns The new row actions ID which the row actions doc can be stored under.
|
||||
*/
|
||||
export function generateRowActionsID(tableId: string) {
|
||||
return `${DocumentType.ROW_ACTIONS}${SEPARATOR}${tableId}`
|
||||
}
|
||||
|
|
|
@ -0,0 +1,248 @@
|
|||
import { Automation, Webhook, WebhookActionType } from "@budibase/types"
|
||||
import { generateAutomationID, getAutomationParams } from "../../../db/utils"
|
||||
import { deleteEntityMetadata } from "../../../utilities"
|
||||
import { MetadataTypes } from "../../../constants"
|
||||
import {
|
||||
context,
|
||||
events,
|
||||
HTTPError,
|
||||
db as dbCore,
|
||||
} from "@budibase/backend-core"
|
||||
import { definitions } from "../../../automations/triggerInfo"
|
||||
import automations from "."
|
||||
|
||||
function getDb() {
|
||||
return context.getAppDB()
|
||||
}
|
||||
|
||||
function cleanAutomationInputs(automation: Automation) {
|
||||
if (automation == null) {
|
||||
return automation
|
||||
}
|
||||
let steps = automation.definition.steps
|
||||
let trigger = automation.definition.trigger
|
||||
let allSteps = [...steps, trigger]
|
||||
// live is not a property used anymore
|
||||
if (automation.live != null) {
|
||||
delete automation.live
|
||||
}
|
||||
for (let step of allSteps) {
|
||||
if (step == null) {
|
||||
continue
|
||||
}
|
||||
for (let inputName of Object.keys(step.inputs)) {
|
||||
if (!step.inputs[inputName] || step.inputs[inputName] === "") {
|
||||
delete step.inputs[inputName]
|
||||
}
|
||||
}
|
||||
}
|
||||
return automation
|
||||
}
|
||||
|
||||
async function handleStepEvents(
|
||||
oldAutomation: Automation,
|
||||
automation: Automation
|
||||
) {
|
||||
const getNewSteps = (oldAutomation: Automation, automation: Automation) => {
|
||||
const oldStepIds = oldAutomation.definition.steps.map(s => s.id)
|
||||
return automation.definition.steps.filter(s => !oldStepIds.includes(s.id))
|
||||
}
|
||||
|
||||
const getDeletedSteps = (
|
||||
oldAutomation: Automation,
|
||||
automation: Automation
|
||||
) => {
|
||||
const stepIds = automation.definition.steps.map(s => s.id)
|
||||
return oldAutomation.definition.steps.filter(s => !stepIds.includes(s.id))
|
||||
}
|
||||
|
||||
// new steps
|
||||
const newSteps = getNewSteps(oldAutomation, automation)
|
||||
for (let step of newSteps) {
|
||||
await events.automation.stepCreated(automation, step)
|
||||
}
|
||||
|
||||
// old steps
|
||||
const deletedSteps = getDeletedSteps(oldAutomation, automation)
|
||||
for (let step of deletedSteps) {
|
||||
await events.automation.stepDeleted(automation, step)
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetch() {
|
||||
const db = getDb()
|
||||
const response = await db.allDocs<Automation>(
|
||||
getAutomationParams(null, {
|
||||
include_docs: true,
|
||||
})
|
||||
)
|
||||
return response.rows.map(row => row.doc)
|
||||
}
|
||||
|
||||
export async function get(automationId: string) {
|
||||
const db = getDb()
|
||||
const result = await db.get<Automation>(automationId)
|
||||
return result
|
||||
}
|
||||
|
||||
export async function create(automation: Automation) {
|
||||
automation = { ...automation }
|
||||
const db = getDb()
|
||||
|
||||
// Respect existing IDs if recreating a deleted automation
|
||||
if (!automation._id) {
|
||||
automation._id = generateAutomationID()
|
||||
}
|
||||
|
||||
automation.type = "automation"
|
||||
automation = cleanAutomationInputs(automation)
|
||||
automation = await checkForWebhooks({
|
||||
newAuto: automation,
|
||||
})
|
||||
const response = await db.put(automation)
|
||||
await events.automation.created(automation)
|
||||
for (let step of automation.definition.steps) {
|
||||
await events.automation.stepCreated(automation, step)
|
||||
}
|
||||
automation._rev = response.rev
|
||||
automation._id = response.id
|
||||
|
||||
return automation
|
||||
}
|
||||
|
||||
export async function update(automation: Automation) {
|
||||
automation = { ...automation }
|
||||
|
||||
if (!automation._id || !automation._rev) {
|
||||
throw new HTTPError("_id or _rev fields missing", 400)
|
||||
}
|
||||
|
||||
const db = getDb()
|
||||
|
||||
const oldAutomation = await db.get<Automation>(automation._id)
|
||||
automation = cleanAutomationInputs(automation)
|
||||
automation = await checkForWebhooks({
|
||||
oldAuto: oldAutomation,
|
||||
newAuto: automation,
|
||||
})
|
||||
const response = await db.put(automation)
|
||||
automation._rev = response.rev
|
||||
|
||||
const oldAutoTrigger =
|
||||
oldAutomation && oldAutomation.definition.trigger
|
||||
? oldAutomation.definition.trigger
|
||||
: undefined
|
||||
const newAutoTrigger =
|
||||
automation && automation.definition.trigger
|
||||
? automation.definition.trigger
|
||||
: undefined
|
||||
// trigger has been updated, remove the test inputs
|
||||
if (oldAutoTrigger && oldAutoTrigger.id !== newAutoTrigger?.id) {
|
||||
await events.automation.triggerUpdated(automation)
|
||||
await deleteEntityMetadata(
|
||||
MetadataTypes.AUTOMATION_TEST_INPUT,
|
||||
automation._id!
|
||||
)
|
||||
}
|
||||
|
||||
await handleStepEvents(oldAutomation, automation)
|
||||
|
||||
return {
|
||||
...automation,
|
||||
_rev: response.rev,
|
||||
_id: response.id,
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove(automationId: string, rev: string) {
|
||||
const db = getDb()
|
||||
const existing = await db.get<Automation>(automationId)
|
||||
await checkForWebhooks({
|
||||
oldAuto: existing,
|
||||
})
|
||||
|
||||
// delete metadata first
|
||||
await deleteEntityMetadata(MetadataTypes.AUTOMATION_TEST_INPUT, automationId)
|
||||
await deleteEntityMetadata(
|
||||
MetadataTypes.AUTOMATION_TEST_HISTORY,
|
||||
automationId
|
||||
)
|
||||
|
||||
const result = await db.remove(automationId, rev)
|
||||
|
||||
await events.automation.deleted(existing)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* This function handles checking if any webhooks need to be created or deleted for automations.
|
||||
* @param appId The ID of the app in which we are checking for webhooks
|
||||
* @param oldAuto The old automation object if updating/deleting
|
||||
* @param newAuto The new automation object if creating/updating
|
||||
* @returns After this is complete the new automation object may have been updated and should be
|
||||
* written to DB (this does not write to DB as it would be wasteful to repeat).
|
||||
*/
|
||||
async function checkForWebhooks({ oldAuto, newAuto }: any) {
|
||||
const WH_STEP_ID = definitions.WEBHOOK.stepId
|
||||
|
||||
const appId = context.getAppId()
|
||||
if (!appId) {
|
||||
throw new Error("Unable to check webhooks - no app ID in context.")
|
||||
}
|
||||
const oldTrigger = oldAuto ? oldAuto.definition.trigger : null
|
||||
const newTrigger = newAuto ? newAuto.definition.trigger : null
|
||||
const triggerChanged =
|
||||
oldTrigger && newTrigger && oldTrigger.id !== newTrigger.id
|
||||
function isWebhookTrigger(auto: any) {
|
||||
return (
|
||||
auto &&
|
||||
auto.definition.trigger &&
|
||||
auto.definition.trigger.stepId === WH_STEP_ID
|
||||
)
|
||||
}
|
||||
// need to delete webhook
|
||||
if (
|
||||
isWebhookTrigger(oldAuto) &&
|
||||
(!isWebhookTrigger(newAuto) || triggerChanged) &&
|
||||
oldTrigger.webhookId
|
||||
) {
|
||||
try {
|
||||
const db = getDb()
|
||||
// need to get the webhook to get the rev
|
||||
const webhook = await db.get<Webhook>(oldTrigger.webhookId)
|
||||
// might be updating - reset the inputs to remove the URLs
|
||||
if (newTrigger) {
|
||||
delete newTrigger.webhookId
|
||||
newTrigger.inputs = {}
|
||||
}
|
||||
await automations.webhook.destroy(webhook._id!, webhook._rev!)
|
||||
} catch (err) {
|
||||
// don't worry about not being able to delete, if it doesn't exist all good
|
||||
}
|
||||
}
|
||||
// need to create webhook
|
||||
if (
|
||||
(!isWebhookTrigger(oldAuto) || triggerChanged) &&
|
||||
isWebhookTrigger(newAuto)
|
||||
) {
|
||||
const webhook = await automations.webhook.save(
|
||||
automations.webhook.newDoc(
|
||||
"Automation webhook",
|
||||
WebhookActionType.AUTOMATION,
|
||||
newAuto._id
|
||||
)
|
||||
)
|
||||
const id = webhook._id
|
||||
newTrigger.webhookId = id
|
||||
// the app ID has to be development for this endpoint
|
||||
// it can only be used when building the app
|
||||
// but the trigger endpoint will always be used in production
|
||||
const prodAppId = dbCore.getProdAppID(appId)
|
||||
newTrigger.inputs = {
|
||||
schemaUrl: `api/webhooks/schema/${appId}/${id}`,
|
||||
triggerUrl: `api/webhooks/trigger/${prodAppId}/${id}`,
|
||||
}
|
||||
}
|
||||
return newAuto
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
import * as crud from "./crud"
|
||||
import * as webhook from "./webhook"
|
||||
import * as utils from "./utils"
|
||||
|
||||
export default {
|
||||
...crud,
|
||||
webhook,
|
||||
utils,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
import { context, HTTPError, utils } from "@budibase/backend-core"
|
||||
|
||||
import { generateRowActionsID } from "../../db/utils"
|
||||
import {
|
||||
SEPARATOR,
|
||||
TableRowActions,
|
||||
VirtualDocumentType,
|
||||
} from "@budibase/types"
|
||||
|
||||
function ensureUniqueAndThrow(
|
||||
doc: TableRowActions,
|
||||
name: string,
|
||||
existingRowActionId?: string
|
||||
) {
|
||||
if (
|
||||
Object.entries(doc.actions).find(
|
||||
([id, a]) =>
|
||||
a.name.toLowerCase() === name.toLowerCase() &&
|
||||
id !== existingRowActionId
|
||||
)
|
||||
) {
|
||||
throw new HTTPError("A row action with the same name already exists.", 409)
|
||||
}
|
||||
}
|
||||
|
||||
export async function create(tableId: string, rowAction: { name: string }) {
|
||||
const action = { name: rowAction.name.trim() }
|
||||
|
||||
const db = context.getAppDB()
|
||||
const rowActionsId = generateRowActionsID(tableId)
|
||||
let doc: TableRowActions
|
||||
try {
|
||||
doc = await db.get<TableRowActions>(rowActionsId)
|
||||
} catch (e: any) {
|
||||
if (e.status !== 404) {
|
||||
throw e
|
||||
}
|
||||
|
||||
doc = { _id: rowActionsId, actions: {} }
|
||||
}
|
||||
|
||||
ensureUniqueAndThrow(doc, action.name)
|
||||
|
||||
const newId = `${VirtualDocumentType.ROW_ACTION}${SEPARATOR}${utils.newid()}`
|
||||
doc.actions[newId] = action
|
||||
await db.put(doc)
|
||||
|
||||
return {
|
||||
id: newId,
|
||||
...action,
|
||||
}
|
||||
}
|
||||
|
||||
export async function get(tableId: string) {
|
||||
const db = context.getAppDB()
|
||||
const rowActionsId = generateRowActionsID(tableId)
|
||||
return await db.get<TableRowActions>(rowActionsId)
|
||||
}
|
||||
|
||||
export async function docExists(tableId: string) {
|
||||
const db = context.getAppDB()
|
||||
const rowActionsId = generateRowActionsID(tableId)
|
||||
const result = await db.exists(rowActionsId)
|
||||
return result
|
||||
}
|
||||
|
||||
export async function update(
|
||||
tableId: string,
|
||||
rowActionId: string,
|
||||
rowAction: { name: string }
|
||||
) {
|
||||
const action = { name: rowAction.name.trim() }
|
||||
const actionsDoc = await get(tableId)
|
||||
|
||||
if (!actionsDoc.actions[rowActionId]) {
|
||||
throw new HTTPError(
|
||||
`Row action '${rowActionId}' not found in '${tableId}'`,
|
||||
400
|
||||
)
|
||||
}
|
||||
|
||||
ensureUniqueAndThrow(actionsDoc, action.name, rowActionId)
|
||||
|
||||
actionsDoc.actions[rowActionId] = action
|
||||
|
||||
const db = context.getAppDB()
|
||||
await db.put(actionsDoc)
|
||||
|
||||
return {
|
||||
id: rowActionId,
|
||||
...action,
|
||||
}
|
||||
}
|
||||
|
||||
export async function remove(tableId: string, rowActionId: string) {
|
||||
const actionsDoc = await get(tableId)
|
||||
|
||||
if (!actionsDoc.actions[rowActionId]) {
|
||||
throw new HTTPError(
|
||||
`Row action '${rowActionId}' not found in '${tableId}'`,
|
||||
400
|
||||
)
|
||||
}
|
||||
|
||||
delete actionsDoc.actions[rowActionId]
|
||||
|
||||
const db = context.getAppDB()
|
||||
await db.put(actionsDoc)
|
||||
}
|
|
@ -10,6 +10,7 @@ import { default as users } from "./users"
|
|||
import { default as plugins } from "./plugins"
|
||||
import * as views from "./app/views"
|
||||
import * as permissions from "./app/permissions"
|
||||
import * as rowActions from "./app/rowActions"
|
||||
|
||||
const sdk = {
|
||||
backups,
|
||||
|
@ -24,6 +25,7 @@ const sdk = {
|
|||
views,
|
||||
permissions,
|
||||
links,
|
||||
rowActions,
|
||||
}
|
||||
|
||||
// default export for TS
|
||||
|
|
|
@ -13,6 +13,7 @@ import { UserAPI } from "./user"
|
|||
import { QueryAPI } from "./query"
|
||||
import { RoleAPI } from "./role"
|
||||
import { TemplateAPI } from "./template"
|
||||
import { RowActionAPI } from "./rowAction"
|
||||
|
||||
export default class API {
|
||||
table: TableAPI
|
||||
|
@ -29,6 +30,7 @@ export default class API {
|
|||
query: QueryAPI
|
||||
roles: RoleAPI
|
||||
templates: TemplateAPI
|
||||
rowAction: RowActionAPI
|
||||
|
||||
constructor(config: TestConfiguration) {
|
||||
this.table = new TableAPI(config)
|
||||
|
@ -45,5 +47,6 @@ export default class API {
|
|||
this.query = new QueryAPI(config)
|
||||
this.roles = new RoleAPI(config)
|
||||
this.templates = new TemplateAPI(config)
|
||||
this.rowAction = new RowActionAPI(config)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
import {
|
||||
CreateRowActionRequest,
|
||||
RowActionResponse,
|
||||
RowActionsResponse,
|
||||
} from "@budibase/types"
|
||||
import { Expectations, TestAPI } from "./base"
|
||||
|
||||
export class RowActionAPI extends TestAPI {
|
||||
save = async (
|
||||
tableId: string,
|
||||
rowAction: CreateRowActionRequest,
|
||||
expectations?: Expectations,
|
||||
config?: { publicUser?: boolean }
|
||||
) => {
|
||||
return await this._post<RowActionResponse>(
|
||||
`/api/tables/${tableId}/actions`,
|
||||
{
|
||||
body: rowAction,
|
||||
expectations: {
|
||||
...expectations,
|
||||
status: expectations?.status || 201,
|
||||
},
|
||||
...config,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
find = async (
|
||||
tableId: string,
|
||||
expectations?: Expectations,
|
||||
config?: { publicUser?: boolean }
|
||||
) => {
|
||||
return await this._get<RowActionsResponse>(
|
||||
`/api/tables/${tableId}/actions`,
|
||||
{
|
||||
expectations,
|
||||
...config,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
update = async (
|
||||
tableId: string,
|
||||
rowActionId: string,
|
||||
rowAction: CreateRowActionRequest,
|
||||
expectations?: Expectations,
|
||||
config?: { publicUser?: boolean }
|
||||
) => {
|
||||
return await this._put<RowActionResponse>(
|
||||
`/api/tables/${tableId}/actions/${rowActionId}`,
|
||||
{
|
||||
body: rowAction,
|
||||
expectations,
|
||||
...config,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
delete = async (
|
||||
tableId: string,
|
||||
rowActionId: string,
|
||||
expectations?: Expectations,
|
||||
config?: { publicUser?: boolean }
|
||||
) => {
|
||||
return await this._delete<RowActionResponse>(
|
||||
`/api/tables/${tableId}/actions/${rowActionId}`,
|
||||
{
|
||||
expectations,
|
||||
...config,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -159,7 +159,7 @@ export function automationTrigger(
|
|||
}
|
||||
|
||||
export function newAutomation({ steps, trigger }: any = {}) {
|
||||
const automation: any = basicAutomation()
|
||||
const automation = basicAutomation()
|
||||
|
||||
if (trigger) {
|
||||
automation.definition.trigger = trigger
|
||||
|
|
|
@ -7,3 +7,4 @@ export * from "./table"
|
|||
export * from "./permission"
|
||||
export * from "./attachment"
|
||||
export * from "./user"
|
||||
export * from "./rowAction"
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
interface RowActionData {
|
||||
name: string
|
||||
}
|
||||
export interface CreateRowActionRequest extends RowActionData {}
|
||||
export interface UpdateRowActionRequest extends RowActionData {}
|
||||
|
||||
export interface RowActionResponse extends RowActionData {
|
||||
id: string
|
||||
tableId: string
|
||||
}
|
||||
|
||||
export interface RowActionsResponse {
|
||||
actions: Record<string, RowActionResponse>
|
||||
}
|
|
@ -16,3 +16,4 @@ export * from "./links"
|
|||
export * from "./component"
|
||||
export * from "./sqlite"
|
||||
export * from "./snippet"
|
||||
export * from "./rowAction"
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import { Document } from "../document"
|
||||
|
||||
export interface TableRowActions extends Document {
|
||||
_id: string
|
||||
actions: Record<
|
||||
string,
|
||||
{
|
||||
name: string
|
||||
}
|
||||
>
|
||||
}
|
|
@ -39,6 +39,7 @@ export enum DocumentType {
|
|||
AUDIT_LOG = "al",
|
||||
APP_MIGRATION_METADATA = "_design/migrations",
|
||||
SCIM_LOG = "scimlog",
|
||||
ROW_ACTIONS = "ra",
|
||||
}
|
||||
|
||||
// these are the core documents that make up the data, design
|
||||
|
@ -68,6 +69,7 @@ export enum InternalTable {
|
|||
// documents or enriched into existence as part of get requests
|
||||
export enum VirtualDocumentType {
|
||||
VIEW = "view",
|
||||
ROW_ACTION = "row_action",
|
||||
}
|
||||
|
||||
export interface Document {
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
const nodemailer = require("nodemailer")
|
||||
|
||||
const options = {
|
||||
port: 587,
|
||||
host: "smtp.ethereal.email",
|
||||
secure: false,
|
||||
auth: {
|
||||
user: "seamus99@ethereal.email",
|
||||
pass: "5ghVteZAqj6jkKJF9R",
|
||||
},
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport(options)
|
||||
transporter.verify(function (error) {
|
||||
if (error) {
|
||||
console.log(error)
|
||||
} else {
|
||||
console.log("Ethereal server is ready to take our messages")
|
||||
}
|
||||
})
|
||||
|
||||
const message = {
|
||||
from: "from@example.com",
|
||||
to: "to@example.com",
|
||||
subject: "Did this email arrive?",
|
||||
html: "Hello World!",
|
||||
}
|
||||
|
||||
transporter.sendMail(message).then(response => {
|
||||
console.log("Test email URL: " + nodemailer.getTestMessageUrl(response))
|
||||
})
|
Loading…
Reference in New Issue