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 17 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
22 changes: 11 additions & 11 deletions packages/backend-core/src/objectStore/objectStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const streamPipeline = promisify(stream.pipeline)
const STATE = {
bucketCreationPromises: {},
}
const signedFilePrefix = "/files/signed"
export const signedFilePrefix = "/files/signed"
PClmnt marked this conversation as resolved.
Show resolved Hide resolved

type ListParams = {
ContinuationToken?: string
Expand All @@ -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 @@ -174,11 +174,9 @@ export async function upload({
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()
}

let contentType = type
Expand Down Expand Up @@ -222,11 +220,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 Expand Up @@ -333,7 +329,11 @@ export function getPresignedUrl(
const signedUrl = new URL(url)
const path = signedUrl.pathname
const query = signedUrl.search
return `${signedFilePrefix}${path}${query}`
if (path.startsWith(signedFilePrefix)) {
return `${path}${query}`
} else {
return `${signedFilePrefix}${path}${query}`
PClmnt marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

Expand Down
56 changes: 54 additions & 2 deletions packages/backend-core/src/objectStore/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { join } from "path"
import path, { join } from "path"
import { tmpdir } from "os"
import fs from "fs"
import env from "../environment"
import { PutBucketLifecycleConfigurationRequest } from "aws-sdk/clients/s3"

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

return params
}

async function processUrlAttachment(
attachment: AutomationAttachment
): Promise<AutomationAttachmentContent> {
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,
}
}

export async function processObjectStoreAttachment(
attachment: AutomationAttachment
): Promise<BucketedContent> {
const result = objectStore.extractBucketAndPath(attachment.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 {
PClmnt marked this conversation as resolved.
Show resolved Hide resolved
bucket,
path,
filename: attachment.filename || fallbackFilename,
content: readStream,
}
}

export async function processAutomationAttachment(
attachment: AutomationAttachment
): Promise<AutomationAttachmentContent | BucketedContent> {
const isFullyFormedUrl =
attachment.url.startsWith("http://") ||
attachment.url.startsWith("https://")
if (isFullyFormedUrl) {
return await processUrlAttachment(attachment)
} else {
return await processObjectStoreAttachment(attachment)
}
}
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 @@ -4,6 +4,7 @@
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "dataBinding"
import { FieldType } from "@budibase/types"

import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
import { createEventDispatcher, setContext } from "svelte"
Expand Down Expand Up @@ -102,6 +103,8 @@
longform: value => !isJSBinding(value),
json: value => !isJSBinding(value),
boolean: isValidBoolean,
attachment: false,
attachment_single: false,
}

const isValid = value => {
Expand All @@ -116,7 +119,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 +169,7 @@
{updateOnChange}
/>
{/if}
{#if !disabled && type !== "formula"}
{#if !disabled && type !== "formula" && !disabled && type !== FieldType.ATTACHMENTS && !disabled && type !== FieldType.ATTACHMENT_SINGLE}
<div
class={`icon ${getIconClass(value, type)}`}
on:click={() => {
Expand Down