Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support attachment columns in Automations #13567

Merged
merged 25 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
735e10b
base work to support attachments in create / update row
PClmnt Apr 25, 2024
9cf3ff4
handle single attachment column
PClmnt Apr 26, 2024
9a893d0
Merge remote-tracking branch 'origin/master' into feat/support-attach…
PClmnt Apr 26, 2024
c328c02
fix tests
PClmnt Apr 26, 2024
70658ce
pro
PClmnt Apr 26, 2024
e7f650c
fix some types
PClmnt Apr 26, 2024
69de016
handle case where file exists in storage
PClmnt Apr 26, 2024
52fb1af
improve attacment processing
PClmnt Apr 26, 2024
3788f7b
refactor slightly and ensure correct url is used for existing attachm…
PClmnt May 3, 2024
58e7853
Merge remote-tracking branch 'origin/master' into feat/support-attach…
PClmnt May 7, 2024
3cf12fe
add test
PClmnt May 8, 2024
c52610c
Fixing a build issue.
mike12345567 May 8, 2024
0c7e5e9
Merge remote-tracking branch 'origin/master' into feat/support-attach…
PClmnt May 8, 2024
bf0e926
update tests
PClmnt May 8, 2024
4db4db8
some lint
PClmnt May 8, 2024
2b7c21e
Merge remote-tracking branch 'origin/master' into feat/support-attach…
PClmnt May 8, 2024
6617a96
remove cursed backend-core test util
PClmnt May 8, 2024
eb6c02b
addressing pr comments
PClmnt May 9, 2024
f42dc82
refactoring nasty automationUtils upload code
PClmnt May 9, 2024
04908ed
remove uneeded check
PClmnt May 9, 2024
dfc5606
use basneeame for fallback filename
PClmnt May 9, 2024
3fd2f96
add a test to ensure coverage of single attachment column type
PClmnt May 9, 2024
afc59b1
Merge remote-tracking branch 'origin/master' into feat/support-attach…
PClmnt May 9, 2024
d1ea38f
fail early when fetching object metadata
PClmnt May 9, 2024
e685775
Merge branch 'master' into feat/support-attachments-in-automations
PClmnt May 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 3 additions & 5 deletions packages/backend-core/src/objectStore/objectStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ type UploadParams = BaseUploadParams & {
}

type StreamUploadParams = BaseUploadParams & {
stream: ReadStream
stream: ReadStream | NodeJS.ReadableStream | ReadableStream<Uint8Array> | null
PClmnt marked this conversation as resolved.
Show resolved Hide resolved
}

const CONTENT_TYPE_MAP: any = {
Expand Down Expand Up @@ -222,11 +222,9 @@ export async function streamUpload({
const objectStore = ObjectStore(bucketName)
const bucketCreated = await createBucketIfNotExists(objectStore, bucketName)

if (ttl && (bucketCreated.created || bucketCreated.exists)) {
if (ttl && bucketCreated.created) {
let ttlConfig = bucketTTLConfig(bucketName, ttl)
if (objectStore.putBucketLifecycleConfiguration) {
await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise()
}
await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise()
PClmnt marked this conversation as resolved.
Show resolved Hide resolved
}

// Set content type for certain known extensions
Expand Down
47 changes: 44 additions & 3 deletions packages/backend-core/src/objectStore/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { join } from "path"
import path, { join } from "path"
import { tmpdir } from "os"
import fs from "fs"
import fs, { ReadStream } from "fs"
import env from "../environment"
import { PutBucketLifecycleConfigurationRequest } from "aws-sdk/clients/s3"

import * as objectStore from "./objectStore"
import { AutomationAttachment } from "@budibase/types"
/****************************************************
* NOTE: When adding a new bucket - name *
* sure that S3 usages (like budibase-infra) *
Expand Down Expand Up @@ -55,3 +56,43 @@ export const bucketTTLConfig = (

return params
}

export const processAutomationAttachment = async (
attachment: AutomationAttachment
): Promise<{
filename: string
content:
| ReadStream
| NodeJS.ReadableStream
| ReadableStream<Uint8Array>
| null
}> => {
const isFullyFormedUrl =
attachment.url.startsWith("http://") ||
attachment.url.startsWith("https://")

if (isFullyFormedUrl) {
const response = await fetch(attachment.url)
if (!response.ok) {
throw new Error(`unexpected response ${response.statusText}`)
}
const fallbackFilename = path.basename(new URL(attachment.url).pathname)
return {
filename: attachment.filename || fallbackFilename,
content: response?.body,
}
} else {
const url = attachment.url
const result = objectStore.extractBucketAndPath(url)
if (result === null) {
throw new Error("Invalid signed URL")
}
const { bucket, path } = result
const readStream = await objectStore.getReadStream(bucket, path)
const fallbackFilename = path.split("/").pop() || ""
return {
filename: attachment.filename || fallbackFilename,
content: readStream,
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,8 @@
value.customType !== "cron" &&
value.customType !== "triggerSchema" &&
value.customType !== "automationFields" &&
value.type !== "attachment"
value.type !== "attachment" &&
value.type !== "attachment_single"
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import { tables } from "stores/builder"
import { Select, Checkbox, Label } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { FieldType } from "@budibase/types"

import RowSelectorTypes from "./RowSelectorTypes.svelte"
import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
Expand All @@ -14,7 +16,6 @@
export let bindings
export let isTestModal
export let isUpdateRow

$: parsedBindings = bindings.map(binding => {
let clone = Object.assign({}, binding)
clone.icon = "ShareAndroid"
Expand All @@ -26,15 +27,19 @@

$: {
table = $tables.list.find(table => table._id === value?.tableId)
schemaFields = Object.entries(table?.schema ?? {})
// surface the schema so the user can see it in the json
schemaFields.map(([, schema]) => {

// Just sorting attachment types to the bottom here for a cleaner UX
schemaFields = Object.entries(table?.schema ?? {}).sort(
([, schemaA], [, schemaB]) =>
(schemaA.type === "attachment") - (schemaB.type === "attachment")
)

schemaFields.forEach(([, schema]) => {
if (!schema.autocolumn && !value[schema.name]) {
value[schema.name] = ""
}
})
}

const onChangeTable = e => {
value["tableId"] = e.detail
dispatch("change", value)
Expand Down Expand Up @@ -114,10 +119,16 @@
</div>
{#if schemaFields.length}
{#each schemaFields as [field, schema]}
{#if !schema.autocolumn && schema.type !== "attachment"}
<div class="schema-fields">
{#if !schema.autocolumn}
<div
class:schema-fields={schema.type !== FieldType.ATTACHMENTS &&
schema.type !== FieldType.ATTACHMENT_SINGLE}
>
<Label>{field}</Label>
<div class="field-width">
<div
class:field-width={schema.type !== FieldType.ATTACHMENTS &&
schema.type !== FieldType.ATTACHMENT_SINGLE}
>
{#if isTestModal}
<RowSelectorTypes
{isTestModal}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<script>
import { Select, DatePicker, Multiselect, TextArea } from "@budibase/bbui"
import { FieldType } from "@budibase/types"
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
import ModalBindableInput from "../../common/bindings/ModalBindableInput.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
import Editor from "components/integration/QueryEditor.svelte"
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"

export let onChange
export let field
Expand All @@ -22,6 +24,16 @@
function schemaHasOptions(schema) {
return !!schema.constraints?.inclusion?.length
}

const handleAttachmentParams = keyValuObj => {
let params = {}
if (keyValuObj?.length) {
for (let param of keyValuObj) {
params[param.url] = param.filename
}
}
return params
}
</script>

{#if schemaHasOptions(schema) && schema.type !== "array"}
Expand Down Expand Up @@ -77,7 +89,31 @@
on:change={e => onChange(e, field)}
useLabel={false}
/>
{:else if ["string", "number", "bigint", "barcodeqr", "array"].includes(schema.type)}
{:else if schema.type === FieldType.ATTACHMENTS || schema.type === FieldType.ATTACHMENT_SINGLE}
<div class="attachment-field-spacinng">
<KeyValueBuilder
on:change={e =>
onChange(
{
detail: e.detail.map(({ name, value }) => ({
url: name,
filename: value,
})),
},
field
)}
object={handleAttachmentParams(value[field])}
allowJS
{bindings}
keyBindings
customButtonText={"Add attachment"}
keyPlaceholder={"URL"}
valuePlaceholder={"Filename"}
noAddButton={schema.type === FieldType.ATTACHMENT_SINGLE &&
value[field].length >= 1}
/>
</div>
{:else if ["string", "number", "bigint", "barcodeqr", "array", "attachment"].includes(schema.type)}
PClmnt marked this conversation as resolved.
Show resolved Hide resolved
<svelte:component
this={isTestModal ? ModalBindableInput : DrawerBindableInput}
panel={AutomationBindingPanel}
Expand All @@ -90,3 +126,10 @@
title={schema.name}
/>
{/if}

<style>
.attachment-field-spacinng {
margin-top: var(--spacing-s);
margin-bottom: var(--spacing-l);
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@
longform: value => !isJSBinding(value),
json: value => !isJSBinding(value),
boolean: isValidBoolean,
attachment: false,
attachment_single: false,
}

const isValid = value => {
Expand All @@ -116,7 +118,16 @@
if (type === "json" && !isJSBinding(value)) {
return "json-slot-icon"
}
if (!["string", "number", "bigint", "barcodeqr"].includes(type)) {
if (
![
"string",
"number",
"bigint",
"barcodeqr",
"attachment",
"attachment_single",
].includes(type)
) {
return "slot-icon"
}
return ""
Expand Down Expand Up @@ -157,7 +168,7 @@
{updateOnChange}
/>
{/if}
{#if !disabled && type !== "formula"}
{#if !disabled && type !== "formula" && !disabled && type !== "attachment" && !disabled && type !== "attachment_single"}
<div
class={`icon ${getIconClass(value, type)}`}
on:click={() => {
Expand Down
50 changes: 49 additions & 1 deletion packages/server/src/automations/automationUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import {
encodeJSBinding,
} from "@budibase/string-templates"
import sdk from "../sdk"
import { Row } from "@budibase/types"
import { AutomationAttachment, Row } from "@budibase/types"
import { LoopInput, LoopStepType } from "../definitions/automations"
import { objectStore, context } from "@budibase/backend-core"
import * as uuid from "uuid"

/**
* When values are input to the system generally they will be of type string as this is required for template strings.
Expand Down Expand Up @@ -96,6 +98,52 @@ export function getError(err: any) {
return typeof err !== "string" ? err.toString() : err
}

export async function sendAutomationAttachmentsToStorage(
tableId: string,
row: Row
) {
let table = await sdk.tables.getTable(tableId)
PClmnt marked this conversation as resolved.
Show resolved Hide resolved
const attachmentRows: { [key: string]: any } = {}

Object.entries(row).forEach(([prop, value]) => {
const schema = table.schema[prop]
if (Object.hasOwn(table.schema, prop) && schema?.type === "attachment") {
attachmentRows[prop] = value
}
})

for (const prop in attachmentRows) {
const attachments = attachmentRows[prop]
const updatedAttachments = await Promise.all(
attachments.map(async (attachment: AutomationAttachment) => {
let { content } = await objectStore.processAutomationAttachment(
attachment
)
const extension = attachment.filename.split(".").pop() || ""
const processedFileName = `${uuid.v4()}.${extension}`
const s3Key = `${context.getProdAppId()}/attachments/${processedFileName}`

if (content) {
await objectStore.streamUpload({
bucket: objectStore.ObjectStoreBuckets.APPS,
stream: content,
filename: s3Key,
})

return {
size: 10,
PClmnt marked this conversation as resolved.
Show resolved Hide resolved
name: attachment.filename,
extension,
key: s3Key,
}
}
})
)
row[prop] = updatedAttachments
}

return row
}
export function substituteLoopStep(hbsString: string, substitute: string) {
let checkForJS = isJSBinding(hbsString)
let substitutedHbsString = ""
Expand Down
11 changes: 10 additions & 1 deletion packages/server/src/automations/steps/createRow.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { save } from "../../api/controllers/row"
import { cleanUpRow, getError } from "../automationUtils"
import {
cleanUpRow,
getError,
sendAutomationAttachmentsToStorage,
} from "../automationUtils"
import { buildCtx } from "./utils"
import {
AutomationActionStepId,
Expand Down Expand Up @@ -89,6 +93,11 @@ export async function run({ inputs, appId, emitter }: AutomationStepInput) {

try {
inputs.row = await cleanUpRow(inputs.row.tableId, inputs.row)

inputs.row = await sendAutomationAttachmentsToStorage(
inputs.row.tableId,
inputs.row
)
await save(ctx)
return {
row: inputs.row,
Expand Down
10 changes: 9 additions & 1 deletion packages/server/src/automations/steps/updateRow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,15 @@ export async function run({ inputs, appId, emitter }: AutomationStepInput) {

try {
if (tableId) {
inputs.row = await automationUtils.cleanUpRow(tableId, inputs.row)
inputs.row = await automationUtils.cleanUpRow(
inputs.row.tableId,
inputs.row
)

inputs.row = await automationUtils.sendAutomationAttachmentsToStorage(
inputs.row.tableId,
inputs.row
)
}
await rowController.patch(ctx)
return {
Expand Down
5 changes: 5 additions & 0 deletions packages/types/src/documents/app/automation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,8 @@ export interface AutomationMetadata extends Document {
errorCount?: number
automationChainCount?: number
}

export type AutomationAttachment = {
url: string
filename: string
}