diff --git a/packages/bbui/src/Form/Core/Dropzone.svelte b/packages/bbui/src/Form/Core/Dropzone.svelte
index 678b7efa0e..c69bf0d6bb 100644
--- a/packages/bbui/src/Form/Core/Dropzone.svelte
+++ b/packages/bbui/src/Form/Core/Dropzone.svelte
@@ -68,7 +68,7 @@
}
$: showDropzone =
- (!maximum || (maximum && value?.length < maximum)) && !disabled
+ (!maximum || (maximum && (value?.length || 0) < maximum)) && !disabled
async function processFileList(fileList) {
if (
diff --git a/packages/builder/src/components/backend/DataTable/formula.js b/packages/builder/src/components/backend/DataTable/formula.js
index e3da4249bc..b339729391 100644
--- a/packages/builder/src/components/backend/DataTable/formula.js
+++ b/packages/builder/src/components/backend/DataTable/formula.js
@@ -9,7 +9,7 @@ const MAX_DEPTH = 1
const TYPES_TO_SKIP = [
FieldType.FORMULA,
FieldType.LONGFORM,
- FieldType.ATTACHMENT,
+ FieldType.ATTACHMENTS,
//https://github.com/Budibase/budibase/issues/3030
FieldType.INTERNAL,
]
diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte
index cfc6e9a7be..92501bec3b 100644
--- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte
+++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte
@@ -394,7 +394,8 @@
FIELDS.BIGINT,
FIELDS.BOOLEAN,
FIELDS.DATETIME,
- FIELDS.ATTACHMENT,
+ FIELDS.ATTACHMENT_SINGLE,
+ FIELDS.ATTACHMENTS,
FIELDS.LINK,
FIELDS.FORMULA,
FIELDS.JSON,
diff --git a/packages/builder/src/components/backend/TableNavigator/ExistingTableDataImport.svelte b/packages/builder/src/components/backend/TableNavigator/ExistingTableDataImport.svelte
index efbfd26565..6901503071 100644
--- a/packages/builder/src/components/backend/TableNavigator/ExistingTableDataImport.svelte
+++ b/packages/builder/src/components/backend/TableNavigator/ExistingTableDataImport.svelte
@@ -1,6 +1,7 @@
@@ -78,14 +86,14 @@
{validation}
{span}
{helpText}
- type="attachment"
+ {type}
bind:fieldState
bind:fieldApi
- defaultValue={[]}
+ {defaultValue}
>
{#if fieldState}
+ import { FieldType } from "@budibase/types"
+ import AttachmentField from "./AttachmentField.svelte"
+
+ const fieldApiMapper = {
+ get: value => (!Array.isArray(value) && value ? [value] : value) || [],
+ set: value => value[0] || null,
+ }
+
+
+
diff --git a/packages/client/src/components/app/forms/index.js b/packages/client/src/components/app/forms/index.js
index 5804d3a79d..aa54204454 100644
--- a/packages/client/src/components/app/forms/index.js
+++ b/packages/client/src/components/app/forms/index.js
@@ -9,6 +9,7 @@ export { default as booleanfield } from "./BooleanField.svelte"
export { default as longformfield } from "./LongFormField.svelte"
export { default as datetimefield } from "./DateTimeField.svelte"
export { default as attachmentfield } from "./AttachmentField.svelte"
+export { default as attachmentsinglefield } from "./AttachmentSingleField.svelte"
export { default as relationshipfield } from "./RelationshipField.svelte"
export { default as passwordfield } from "./PasswordField.svelte"
export { default as formstep } from "./FormStep.svelte"
diff --git a/packages/client/src/components/app/forms/validation.js b/packages/client/src/components/app/forms/validation.js
index 3b3a5d6e1d..cdedd85cf2 100644
--- a/packages/client/src/components/app/forms/validation.js
+++ b/packages/client/src/components/app/forms/validation.js
@@ -192,7 +192,7 @@ const parseType = (value, type) => {
}
// Parse attachments, treating no elements as null
- if (type === FieldTypes.ATTACHMENT) {
+ if (type === FieldTypes.ATTACHMENTS) {
if (!Array.isArray(value) || !value.length) {
return null
}
diff --git a/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte b/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte
index a1f5c4f2aa..3a1f165b6e 100644
--- a/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte
+++ b/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte
@@ -10,6 +10,7 @@
export let invertX = false
export let invertY = false
export let schema
+ export let maximum
const { API, notifications } = getContext("grid")
const imageExtensions = ["png", "tiff", "gif", "raw", "jpg", "jpeg"]
@@ -98,7 +99,7 @@
{value}
compact
on:change={e => onChange(e.detail)}
- maximum={schema.constraints?.length?.maximum}
+ maximum={maximum || schema.constraints?.length?.maximum}
{processFiles}
{deleteAttachments}
{handleFileTooLarge}
diff --git a/packages/frontend-core/src/components/grid/cells/AttachmentSingleCell.svelte b/packages/frontend-core/src/components/grid/cells/AttachmentSingleCell.svelte
new file mode 100644
index 0000000000..e3e7891b7c
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/cells/AttachmentSingleCell.svelte
@@ -0,0 +1,22 @@
+
+
+
diff --git a/packages/frontend-core/src/components/grid/lib/renderers.js b/packages/frontend-core/src/components/grid/lib/renderers.js
index 19bf63312d..c3ee276ff9 100644
--- a/packages/frontend-core/src/components/grid/lib/renderers.js
+++ b/packages/frontend-core/src/components/grid/lib/renderers.js
@@ -11,6 +11,7 @@ import BooleanCell from "../cells/BooleanCell.svelte"
import FormulaCell from "../cells/FormulaCell.svelte"
import JSONCell from "../cells/JSONCell.svelte"
import AttachmentCell from "../cells/AttachmentCell.svelte"
+import AttachmentSingleCell from "../cells/AttachmentSingleCell.svelte"
import BBReferenceCell from "../cells/BBReferenceCell.svelte"
const TypeComponentMap = {
@@ -22,7 +23,8 @@ const TypeComponentMap = {
[FieldType.ARRAY]: MultiSelectCell,
[FieldType.NUMBER]: NumberCell,
[FieldType.BOOLEAN]: BooleanCell,
- [FieldType.ATTACHMENT]: AttachmentCell,
+ [FieldType.ATTACHMENTS]: AttachmentCell,
+ [FieldType.ATTACHMENT_SINGLE]: AttachmentSingleCell,
[FieldType.LINK]: RelationshipCell,
[FieldType.FORMULA]: FormulaCell,
[FieldType.JSON]: JSONCell,
diff --git a/packages/frontend-core/src/constants.js b/packages/frontend-core/src/constants.js
index 79b35bfb8e..95228c3bdc 100644
--- a/packages/frontend-core/src/constants.js
+++ b/packages/frontend-core/src/constants.js
@@ -124,7 +124,8 @@ export const TypeIconMap = {
[FieldType.ARRAY]: "Duplicate",
[FieldType.NUMBER]: "123",
[FieldType.BOOLEAN]: "Boolean",
- [FieldType.ATTACHMENT]: "AppleFiles",
+ [FieldType.ATTACHMENTS]: "Attach",
+ [FieldType.ATTACHMENT_SINGLE]: "Attach",
[FieldType.LINK]: "DataCorrelated",
[FieldType.FORMULA]: "Calculator",
[FieldType.JSON]: "Brackets",
diff --git a/packages/pro b/packages/pro
index f8e8f87bd5..ef186d0024 160000
--- a/packages/pro
+++ b/packages/pro
@@ -1 +1 @@
-Subproject commit f8e8f87bd52081e1303a5ae92c432ea5b38f3bb4
+Subproject commit ef186d00241f96037f9fd34d7a3826041977ab3a
diff --git a/packages/server/src/api/controllers/table/utils.ts b/packages/server/src/api/controllers/table/utils.ts
index c00a8c1bc2..f496c686f3 100644
--- a/packages/server/src/api/controllers/table/utils.ts
+++ b/packages/server/src/api/controllers/table/utils.ts
@@ -30,8 +30,6 @@ import {
View,
RelationshipFieldMetadata,
FieldType,
- FieldTypeSubtypes,
- AttachmentFieldMetadata,
} from "@budibase/types"
import sdk from "../../../sdk"
import env from "../../../environment"
@@ -93,26 +91,6 @@ export async function checkForColumnUpdates(
await checkForViewUpdates(updatedTable, deletedColumns, columnRename)
}
- const changedAttachmentSubtypeColumns = Object.values(
- updatedTable.schema
- ).filter(
- (column): column is AttachmentFieldMetadata =>
- column.type === FieldType.ATTACHMENT &&
- column.subtype !== oldTable?.schema[column.name]?.subtype
- )
- for (const attachmentColumn of changedAttachmentSubtypeColumns) {
- if (attachmentColumn.subtype === FieldTypeSubtypes.ATTACHMENT.SINGLE) {
- attachmentColumn.constraints ??= { length: {} }
- attachmentColumn.constraints.length ??= {}
- attachmentColumn.constraints.length.maximum = 1
- attachmentColumn.constraints.length.message =
- "cannot contain multiple files"
- } else {
- delete attachmentColumn.constraints?.length?.maximum
- delete attachmentColumn.constraints?.length?.message
- }
- }
-
return { rows: updatedRows, table: updatedTable }
}
diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts
index db10901367..8e1014f825 100644
--- a/packages/server/src/api/routes/tests/row.spec.ts
+++ b/packages/server/src/api/routes/tests/row.spec.ts
@@ -6,14 +6,17 @@ import * as setup from "./utilities"
import { context, InternalTable, tenancy } from "@budibase/backend-core"
import { quotas } from "@budibase/pro"
import {
+ AttachmentFieldMetadata,
AutoFieldSubType,
Datasource,
+ DateFieldMetadata,
DeleteRow,
FieldSchema,
FieldType,
FieldTypeSubtypes,
FormulaType,
INTERNAL_TABLE_SOURCE_ID,
+ NumberFieldMetadata,
QuotaUsageType,
RelationshipType,
Row,
@@ -232,9 +235,14 @@ describe.each([
name: "str",
constraints: { type: "string", presence: false },
}
- const attachment: FieldSchema = {
- type: FieldType.ATTACHMENT,
- name: "attachment",
+ const singleAttachment: FieldSchema = {
+ type: FieldType.ATTACHMENT_SINGLE,
+ name: "single attachment",
+ constraints: { presence: false },
+ }
+ const attachmentList: AttachmentFieldMetadata = {
+ type: FieldType.ATTACHMENTS,
+ name: "attachments",
constraints: { type: "array", presence: false },
}
const bool: FieldSchema = {
@@ -242,12 +250,12 @@ describe.each([
name: "boolean",
constraints: { type: "boolean", presence: false },
}
- const number: FieldSchema = {
+ const number: NumberFieldMetadata = {
type: FieldType.NUMBER,
name: "str",
constraints: { type: "number", presence: false },
}
- const datetime: FieldSchema = {
+ const datetime: DateFieldMetadata = {
type: FieldType.DATETIME,
name: "datetime",
constraints: {
@@ -297,10 +305,12 @@ describe.each([
boolUndefined: bool,
boolString: bool,
boolBool: bool,
- attachmentNull: attachment,
- attachmentUndefined: attachment,
- attachmentEmpty: attachment,
- attachmentEmptyArrayStr: attachment,
+ singleAttachmentNull: singleAttachment,
+ singleAttachmentUndefined: singleAttachment,
+ attachmentListNull: attachmentList,
+ attachmentListUndefined: attachmentList,
+ attachmentListEmpty: attachmentList,
+ attachmentListEmptyArrayStr: attachmentList,
arrayFieldEmptyArrayStr: arrayField,
arrayFieldArrayStrKnown: arrayField,
arrayFieldNull: arrayField,
@@ -336,10 +346,12 @@ describe.each([
boolString: "true",
boolBool: true,
tableId: table._id,
- attachmentNull: null,
- attachmentUndefined: undefined,
- attachmentEmpty: "",
- attachmentEmptyArrayStr: "[]",
+ singleAttachmentNull: null,
+ singleAttachmentUndefined: undefined,
+ attachmentListNull: null,
+ attachmentListUndefined: undefined,
+ attachmentListEmpty: "",
+ attachmentListEmptyArrayStr: "[]",
arrayFieldEmptyArrayStr: "[]",
arrayFieldUndefined: undefined,
arrayFieldNull: null,
@@ -368,10 +380,12 @@ describe.each([
expect(row.boolUndefined).toBe(undefined)
expect(row.boolString).toBe(true)
expect(row.boolBool).toBe(true)
- expect(row.attachmentNull).toEqual([])
- expect(row.attachmentUndefined).toBe(undefined)
- expect(row.attachmentEmpty).toEqual([])
- expect(row.attachmentEmptyArrayStr).toEqual([])
+ expect(row.singleAttachmentNull).toEqual(null)
+ expect(row.singleAttachmentUndefined).toBe(undefined)
+ expect(row.attachmentListNull).toEqual([])
+ expect(row.attachmentListUndefined).toBe(undefined)
+ expect(row.attachmentListEmpty).toEqual([])
+ expect(row.attachmentListEmptyArrayStr).toEqual([])
expect(row.arrayFieldEmptyArrayStr).toEqual([])
expect(row.arrayFieldNull).toEqual([])
expect(row.arrayFieldUndefined).toEqual(undefined)
@@ -817,12 +831,44 @@ describe.each([
isInternal &&
describe("attachments", () => {
- it("should allow enriching attachment rows", async () => {
+ it("should allow enriching single attachment rows", async () => {
const table = await config.api.table.save(
defaultTable({
schema: {
attachment: {
- type: FieldType.ATTACHMENT,
+ type: FieldType.ATTACHMENT_SINGLE,
+ name: "attachment",
+ constraints: { presence: false },
+ },
+ },
+ })
+ )
+ const attachmentId = `${uuid.v4()}.csv`
+ const row = await config.api.row.save(table._id!, {
+ name: "test",
+ description: "test",
+ attachment: {
+ key: `${config.getAppId()}/attachments/${attachmentId}`,
+ },
+
+ tableId: table._id,
+ })
+ await config.withEnv({ SELF_HOSTED: "true" }, async () => {
+ return context.doInAppContext(config.getAppId(), async () => {
+ const enriched = await outputProcessing(table, [row])
+ expect((enriched as Row[])[0].attachment.url).toBe(
+ `/files/signed/prod-budi-app-assets/${config.getProdAppId()}/attachments/${attachmentId}`
+ )
+ })
+ })
+ })
+
+ it("should allow enriching attachment list rows", async () => {
+ const table = await config.api.table.save(
+ defaultTable({
+ schema: {
+ attachment: {
+ type: FieldType.ATTACHMENTS,
name: "attachment",
constraints: { type: "array", presence: false },
},
diff --git a/packages/server/src/db/defaultData/datasource_bb_default.ts b/packages/server/src/db/defaultData/datasource_bb_default.ts
index 03aed3c118..68d49b2d8b 100644
--- a/packages/server/src/db/defaultData/datasource_bb_default.ts
+++ b/packages/server/src/db/defaultData/datasource_bb_default.ts
@@ -299,7 +299,7 @@ export const DEFAULT_EMPLOYEE_TABLE_SCHEMA: Table = {
sortable: false,
},
"Badge Photo": {
- type: FieldType.ATTACHMENT,
+ type: FieldType.ATTACHMENTS,
constraints: {
type: FieldType.ARRAY,
presence: false,
@@ -607,7 +607,7 @@ export const DEFAULT_EXPENSES_TABLE_SCHEMA: Table = {
ignoreTimezones: true,
},
Attachment: {
- type: FieldType.ATTACHMENT,
+ type: FieldType.ATTACHMENTS,
constraints: {
type: FieldType.ARRAY,
presence: false,
diff --git a/packages/server/src/sdk/app/backups/imports.ts b/packages/server/src/sdk/app/backups/imports.ts
index 977dea5b63..1c08c8c3bf 100644
--- a/packages/server/src/sdk/app/backups/imports.ts
+++ b/packages/server/src/sdk/app/backups/imports.ts
@@ -5,6 +5,7 @@ import {
Automation,
AutomationTriggerStepId,
RowAttachment,
+ FieldType,
} from "@budibase/types"
import { getAutomationParams } from "../../../db/utils"
import { budibaseTempDir } from "../../../utilities/budibaseDir"
@@ -58,10 +59,19 @@ export async function updateAttachmentColumns(prodAppId: string, db: Database) {
updatedRows = updatedRows.concat(
rows.map(row => {
for (let column of columns) {
- if (Array.isArray(row[column])) {
+ const columnType = table.schema[column].type
+ if (
+ columnType === FieldType.ATTACHMENTS &&
+ Array.isArray(row[column])
+ ) {
row[column] = row[column].map((attachment: RowAttachment) =>
rewriteAttachmentUrl(prodAppId, attachment)
)
+ } else if (
+ columnType === FieldType.ATTACHMENT_SINGLE &&
+ row[column]
+ ) {
+ row[column] = rewriteAttachmentUrl(prodAppId, row[column])
}
}
return row
diff --git a/packages/server/src/sdk/app/rows/attachments.ts b/packages/server/src/sdk/app/rows/attachments.ts
index 2ab9e83c47..ee816ea1fc 100644
--- a/packages/server/src/sdk/app/rows/attachments.ts
+++ b/packages/server/src/sdk/app/rows/attachments.ts
@@ -30,7 +30,10 @@ export async function getRowsWithAttachments(appId: string, table: Table) {
const db = dbCore.getDB(appId)
const attachmentCols: string[] = []
for (let [key, column] of Object.entries(table.schema)) {
- if (column.type === FieldType.ATTACHMENT) {
+ if (
+ column.type === FieldType.ATTACHMENTS ||
+ column.type === FieldType.ATTACHMENT_SINGLE
+ ) {
attachmentCols.push(key)
}
}
diff --git a/packages/server/src/sdk/app/rows/utils.ts b/packages/server/src/sdk/app/rows/utils.ts
index b7adf7131c..d307b17947 100644
--- a/packages/server/src/sdk/app/rows/utils.ts
+++ b/packages/server/src/sdk/app/rows/utils.ts
@@ -175,13 +175,13 @@ export async function validate({
errors[fieldName] = [`${fieldName} is required`]
}
} else if (
- (type === FieldType.ATTACHMENT || type === FieldType.JSON) &&
+ (type === FieldType.ATTACHMENTS || type === FieldType.JSON) &&
typeof row[fieldName] === "string"
) {
// this should only happen if there is an error
try {
const json = JSON.parse(row[fieldName])
- if (type === FieldType.ATTACHMENT) {
+ if (type === FieldType.ATTACHMENTS) {
if (Array.isArray(json)) {
row[fieldName] = json
} else {
diff --git a/packages/server/src/sdk/app/tables/internal/sqs.ts b/packages/server/src/sdk/app/tables/internal/sqs.ts
index da947c62c2..79d9be2348 100644
--- a/packages/server/src/sdk/app/tables/internal/sqs.ts
+++ b/packages/server/src/sdk/app/tables/internal/sqs.ts
@@ -27,7 +27,8 @@ const FieldTypeMap: Record = {
[FieldType.JSON]: SQLiteType.BLOB,
[FieldType.INTERNAL]: SQLiteType.BLOB,
[FieldType.BARCODEQR]: SQLiteType.BLOB,
- [FieldType.ATTACHMENT]: SQLiteType.BLOB,
+ [FieldType.ATTACHMENTS]: SQLiteType.BLOB,
+ [FieldType.ATTACHMENT_SINGLE]: SQLiteType.BLOB,
[FieldType.ARRAY]: SQLiteType.BLOB,
[FieldType.LINK]: SQLiteType.BLOB,
[FieldType.BIGINT]: SQLiteType.REAL,
diff --git a/packages/server/src/sdk/tests/attachments.spec.ts b/packages/server/src/sdk/tests/attachments.spec.ts
index c1736e6f8e..46165f68fc 100644
--- a/packages/server/src/sdk/tests/attachments.spec.ts
+++ b/packages/server/src/sdk/tests/attachments.spec.ts
@@ -31,9 +31,13 @@ describe("should be able to re-write attachment URLs", () => {
sourceType: TableSourceType.INTERNAL,
schema: {
photo: {
- type: FieldType.ATTACHMENT,
+ type: FieldType.ATTACHMENT_SINGLE,
name: "photo",
},
+ gallery: {
+ type: FieldType.ATTACHMENTS,
+ name: "gallery",
+ },
otherCol: {
type: FieldType.STRING,
name: "otherCol",
@@ -43,7 +47,8 @@ describe("should be able to re-write attachment URLs", () => {
for (let i = 0; i < FIND_LIMIT * 4; i++) {
await config.api.row.save(table._id!, {
- photo: [attachment],
+ photo: { ...attachment },
+ gallery: [{ ...attachment }, { ...attachment }],
otherCol: "string",
})
}
@@ -56,8 +61,12 @@ describe("should be able to re-write attachment URLs", () => {
)
for (const row of rows) {
expect(row.otherCol).toBe("string")
- expect(row.photo[0].url).toBe("")
- expect(row.photo[0].key).toBe(`${db.name}/attachments/a.png`)
+ expect(row.photo.url).toBe("")
+ expect(row.photo.key).toBe(`${db.name}/attachments/a.png`)
+ expect(row.gallery[0].url).toBe("")
+ expect(row.gallery[0].key).toBe(`${db.name}/attachments/a.png`)
+ expect(row.gallery[1].url).toBe("")
+ expect(row.gallery[1].key).toBe(`${db.name}/attachments/a.png`)
}
})
})
diff --git a/packages/server/src/utilities/rowProcessor/attachments.ts b/packages/server/src/utilities/rowProcessor/attachments.ts
index e1c83352d4..da52d6a631 100644
--- a/packages/server/src/utilities/rowProcessor/attachments.ts
+++ b/packages/server/src/utilities/rowProcessor/attachments.ts
@@ -1,12 +1,6 @@
import { ObjectStoreBuckets } from "../../constants"
import { context, db as dbCore, objectStore } from "@budibase/backend-core"
-import {
- FieldType,
- RenameColumn,
- Row,
- RowAttachment,
- Table,
-} from "@budibase/types"
+import { FieldType, RenameColumn, Row, Table } from "@budibase/types"
export class AttachmentCleanup {
static async coreCleanup(fileListFn: () => string[]): Promise {
@@ -25,6 +19,27 @@ export class AttachmentCleanup {
}
}
+ private static extractAttachmentKeys(
+ type: FieldType,
+ rowData: any
+ ): string[] {
+ if (
+ type !== FieldType.ATTACHMENTS &&
+ type !== FieldType.ATTACHMENT_SINGLE
+ ) {
+ return []
+ }
+
+ if (!rowData) {
+ return []
+ }
+
+ if (type === FieldType.ATTACHMENTS) {
+ return rowData.map((attachment: any) => attachment.key)
+ }
+ return [rowData.key]
+ }
+
private static async tableChange(
table: Table,
rows: Row[],
@@ -34,16 +49,20 @@ export class AttachmentCleanup {
let files: string[] = []
const tableSchema = opts.oldTable?.schema || table.schema
for (let [key, schema] of Object.entries(tableSchema)) {
- if (schema.type !== FieldType.ATTACHMENT) {
+ if (
+ schema.type !== FieldType.ATTACHMENTS &&
+ schema.type !== FieldType.ATTACHMENT_SINGLE
+ ) {
continue
}
+
const columnRemoved = opts.oldTable && !table.schema[key]
const renaming = opts.rename?.old === key
// old table had this column, new table doesn't - delete it
if ((columnRemoved && !renaming) || opts.deleting) {
rows.forEach(row => {
files = files.concat(
- (row[key] || []).map((attachment: any) => attachment.key)
+ AttachmentCleanup.extractAttachmentKeys(schema.type, row[key])
)
})
}
@@ -68,15 +87,15 @@ export class AttachmentCleanup {
return AttachmentCleanup.coreCleanup(() => {
let files: string[] = []
for (let [key, schema] of Object.entries(table.schema)) {
- if (schema.type !== FieldType.ATTACHMENT) {
+ if (
+ schema.type !== FieldType.ATTACHMENTS &&
+ schema.type !== FieldType.ATTACHMENT_SINGLE
+ ) {
continue
}
rows.forEach(row => {
- if (!Array.isArray(row[key])) {
- return
- }
files = files.concat(
- row[key].map((attachment: any) => attachment.key)
+ AttachmentCleanup.extractAttachmentKeys(schema.type, row[key])
)
})
}
@@ -88,16 +107,21 @@ export class AttachmentCleanup {
return AttachmentCleanup.coreCleanup(() => {
let files: string[] = []
for (let [key, schema] of Object.entries(table.schema)) {
- if (schema.type !== FieldType.ATTACHMENT) {
+ if (
+ schema.type !== FieldType.ATTACHMENTS &&
+ schema.type !== FieldType.ATTACHMENT_SINGLE
+ ) {
continue
}
- const oldKeys =
- opts.oldRow[key]?.map(
- (attachment: RowAttachment) => attachment.key
- ) || []
- const newKeys =
- opts.row[key]?.map((attachment: RowAttachment) => attachment.key) ||
- []
+
+ const oldKeys = AttachmentCleanup.extractAttachmentKeys(
+ schema.type,
+ opts.oldRow[key]
+ )
+ const newKeys = AttachmentCleanup.extractAttachmentKeys(
+ schema.type,
+ opts.row[key]
+ )
files = files.concat(
oldKeys.filter((key: string) => newKeys.indexOf(key) === -1)
)
diff --git a/packages/server/src/utilities/rowProcessor/index.ts b/packages/server/src/utilities/rowProcessor/index.ts
index 0015680e77..e69cfa471a 100644
--- a/packages/server/src/utilities/rowProcessor/index.ts
+++ b/packages/server/src/utilities/rowProcessor/index.ts
@@ -148,13 +148,18 @@ export async function inputProcessing(
}
// remove any attachment urls, they are generated on read
- if (field.type === FieldType.ATTACHMENT) {
+ if (field.type === FieldType.ATTACHMENTS) {
const attachments = clonedRow[key]
if (attachments?.length) {
attachments.forEach((attachment: RowAttachment) => {
delete attachment.url
})
}
+ } else if (field.type === FieldType.ATTACHMENT_SINGLE) {
+ const attachment = clonedRow[key]
+ if (attachment?.url) {
+ delete clonedRow[key].url
+ }
}
if (field.type === FieldType.BB_REFERENCE && value) {
@@ -216,7 +221,7 @@ export async function outputProcessing(
// process complex types: attachements, bb references...
for (let [property, column] of Object.entries(table.schema)) {
- if (column.type === FieldType.ATTACHMENT) {
+ if (column.type === FieldType.ATTACHMENTS) {
for (let row of enriched) {
if (row[property] == null || !Array.isArray(row[property])) {
continue
@@ -227,6 +232,16 @@ export async function outputProcessing(
}
})
}
+ } else if (column.type === FieldType.ATTACHMENT_SINGLE) {
+ for (let row of enriched) {
+ if (!row[property]) {
+ continue
+ }
+
+ if (!row[property].url) {
+ row[property].url = objectStore.getAppFileUrl(row[property].key)
+ }
+ }
} else if (
!opts.skipBBReferences &&
column.type == FieldType.BB_REFERENCE
diff --git a/packages/server/src/utilities/rowProcessor/map.ts b/packages/server/src/utilities/rowProcessor/map.ts
index 60fe5a001b..2e0ac9efe1 100644
--- a/packages/server/src/utilities/rowProcessor/map.ts
+++ b/packages/server/src/utilities/rowProcessor/map.ts
@@ -106,7 +106,7 @@ export const TYPE_TRANSFORM_MAP: any = {
return date
},
},
- [FieldType.ATTACHMENT]: {
+ [FieldType.ATTACHMENTS]: {
//@ts-ignore
[null]: [],
//@ts-ignore
diff --git a/packages/server/src/utilities/rowProcessor/tests/attachments.spec.ts b/packages/server/src/utilities/rowProcessor/tests/attachments.spec.ts
index cefea7e504..3ef8c71afc 100644
--- a/packages/server/src/utilities/rowProcessor/tests/attachments.spec.ts
+++ b/packages/server/src/utilities/rowProcessor/tests/attachments.spec.ts
@@ -25,121 +25,155 @@ const mockedDeleteFiles = objectStore.deleteFiles as jest.MockedFunction<
typeof objectStore.deleteFiles
>
-function table(): Table {
- return {
- name: "table",
- sourceId: DEFAULT_BB_DATASOURCE_ID,
- sourceType: TableSourceType.INTERNAL,
- type: "table",
- schema: {
- attach: {
- name: "attach",
- type: FieldType.ATTACHMENT,
- constraints: {},
- },
+const rowGenerators: [
+ string,
+ FieldType.ATTACHMENT_SINGLE | FieldType.ATTACHMENTS,
+ (fileKey?: string) => Row
+][] = [
+ [
+ "row with a attachment list column",
+ FieldType.ATTACHMENTS,
+ function rowWithAttachments(fileKey: string = FILE_NAME): Row {
+ return {
+ attach: [
+ {
+ size: 1,
+ extension: "jpg",
+ key: fileKey,
+ },
+ ],
+ }
},
- }
-}
-
-function row(fileKey: string = FILE_NAME): Row {
- return {
- attach: [
- {
- size: 1,
- extension: "jpg",
- key: fileKey,
- },
- ],
- }
-}
-
-describe("attachment cleanup", () => {
- beforeEach(() => {
- mockedDeleteFiles.mockClear()
- })
-
- it("should be able to cleanup a table update", async () => {
- const originalTable = table()
- delete originalTable.schema["attach"]
- await AttachmentCleanup.tableUpdate(originalTable, [row()], {
- oldTable: table(),
- })
- expect(mockedDeleteFiles).toHaveBeenCalledWith(BUCKET, [FILE_NAME])
- })
-
- it("should be able to cleanup a table deletion", async () => {
- await AttachmentCleanup.tableDelete(table(), [row()])
- expect(mockedDeleteFiles).toHaveBeenCalledWith(BUCKET, [FILE_NAME])
- })
-
- it("should handle table column renaming", async () => {
- const updatedTable = table()
- updatedTable.schema.attach2 = updatedTable.schema.attach
- delete updatedTable.schema.attach
- await AttachmentCleanup.tableUpdate(updatedTable, [row()], {
- oldTable: table(),
- rename: { old: "attach", updated: "attach2" },
- })
- expect(mockedDeleteFiles).not.toHaveBeenCalled()
- })
-
- it("shouldn't cleanup if no table changes", async () => {
- await AttachmentCleanup.tableUpdate(table(), [row()], { oldTable: table() })
- expect(mockedDeleteFiles).not.toHaveBeenCalled()
- })
-
- it("should handle row updates", async () => {
- const updatedRow = row()
- delete updatedRow.attach
- await AttachmentCleanup.rowUpdate(table(), {
- row: updatedRow,
- oldRow: row(),
- })
- expect(mockedDeleteFiles).toHaveBeenCalledWith(BUCKET, [FILE_NAME])
- })
-
- it("should handle row deletion", async () => {
- await AttachmentCleanup.rowDelete(table(), [row()])
- expect(mockedDeleteFiles).toHaveBeenCalledWith(BUCKET, [FILE_NAME])
- })
-
- it("should handle row deletion and not throw when attachments are undefined", async () => {
- await AttachmentCleanup.rowDelete(table(), [
- {
- attach: undefined,
- },
- ])
- })
-
- it("shouldn't cleanup attachments if row not updated", async () => {
- await AttachmentCleanup.rowUpdate(table(), { row: row(), oldRow: row() })
- expect(mockedDeleteFiles).not.toHaveBeenCalled()
- })
-
- it("should be able to cleanup a column and not throw when attachments are undefined", async () => {
- const originalTable = table()
- delete originalTable.schema["attach"]
- await AttachmentCleanup.tableUpdate(
- originalTable,
- [row("file 1"), { attach: undefined }, row("file 2")],
- {
- oldTable: table(),
+ ],
+ [
+ "row with a single attachment column",
+ FieldType.ATTACHMENT_SINGLE,
+ function rowWithAttachments(fileKey: string = FILE_NAME): Row {
+ return {
+ attach: {
+ size: 1,
+ extension: "jpg",
+ key: fileKey,
+ },
}
- )
- expect(mockedDeleteFiles).toHaveBeenCalledTimes(1)
- expect(mockedDeleteFiles).toHaveBeenCalledWith(BUCKET, ["file 1", "file 2"])
- })
+ },
+ ],
+]
- it("should be able to cleanup a column and not throw when ALL attachments are undefined", async () => {
- const originalTable = table()
- delete originalTable.schema["attach"]
- await AttachmentCleanup.tableUpdate(
- originalTable,
- [{}, { attach: undefined }],
- {
- oldTable: table(),
+describe.each(rowGenerators)(
+ "attachment cleanup",
+ (_, attachmentFieldType, rowGenerator) => {
+ function tableGenerator(): Table {
+ return {
+ name: "table",
+ sourceId: DEFAULT_BB_DATASOURCE_ID,
+ sourceType: TableSourceType.INTERNAL,
+ type: "table",
+ schema: {
+ attach: {
+ name: "attach",
+ type: attachmentFieldType,
+ constraints: {},
+ },
+ },
}
- )
- expect(mockedDeleteFiles).not.toHaveBeenCalled()
- })
-})
+ }
+
+ beforeEach(() => {
+ mockedDeleteFiles.mockClear()
+ })
+
+ it("should be able to cleanup a table update", async () => {
+ const originalTable = tableGenerator()
+ delete originalTable.schema["attach"]
+ await AttachmentCleanup.tableUpdate(originalTable, [rowGenerator()], {
+ oldTable: tableGenerator(),
+ })
+ expect(mockedDeleteFiles).toHaveBeenCalledWith(BUCKET, [FILE_NAME])
+ })
+
+ it("should be able to cleanup a table deletion", async () => {
+ await AttachmentCleanup.tableDelete(tableGenerator(), [rowGenerator()])
+ expect(mockedDeleteFiles).toHaveBeenCalledWith(BUCKET, [FILE_NAME])
+ })
+
+ it("should handle table column renaming", async () => {
+ const updatedTable = tableGenerator()
+ updatedTable.schema.attach2 = updatedTable.schema.attach
+ delete updatedTable.schema.attach
+ await AttachmentCleanup.tableUpdate(updatedTable, [rowGenerator()], {
+ oldTable: tableGenerator(),
+ rename: { old: "attach", updated: "attach2" },
+ })
+ expect(mockedDeleteFiles).not.toHaveBeenCalled()
+ })
+
+ it("shouldn't cleanup if no table changes", async () => {
+ await AttachmentCleanup.tableUpdate(tableGenerator(), [rowGenerator()], {
+ oldTable: tableGenerator(),
+ })
+ expect(mockedDeleteFiles).not.toHaveBeenCalled()
+ })
+
+ it("should handle row updates", async () => {
+ const updatedRow = rowGenerator()
+ delete updatedRow.attach
+ await AttachmentCleanup.rowUpdate(tableGenerator(), {
+ row: updatedRow,
+ oldRow: rowGenerator(),
+ })
+ expect(mockedDeleteFiles).toHaveBeenCalledWith(BUCKET, [FILE_NAME])
+ })
+
+ it("should handle row deletion", async () => {
+ await AttachmentCleanup.rowDelete(tableGenerator(), [rowGenerator()])
+ expect(mockedDeleteFiles).toHaveBeenCalledWith(BUCKET, [FILE_NAME])
+ })
+
+ it("should handle row deletion and not throw when attachments are undefined", async () => {
+ await AttachmentCleanup.rowDelete(tableGenerator(), [
+ {
+ multipleAttachments: undefined,
+ },
+ ])
+ })
+
+ it("shouldn't cleanup attachments if row not updated", async () => {
+ await AttachmentCleanup.rowUpdate(tableGenerator(), {
+ row: rowGenerator(),
+ oldRow: rowGenerator(),
+ })
+ expect(mockedDeleteFiles).not.toHaveBeenCalled()
+ })
+
+ it("should be able to cleanup a column and not throw when attachments are undefined", async () => {
+ const originalTable = tableGenerator()
+ delete originalTable.schema["attach"]
+ await AttachmentCleanup.tableUpdate(
+ originalTable,
+ [rowGenerator("file 1"), { attach: undefined }, rowGenerator("file 2")],
+ {
+ oldTable: tableGenerator(),
+ }
+ )
+ expect(mockedDeleteFiles).toHaveBeenCalledTimes(1)
+ expect(mockedDeleteFiles).toHaveBeenCalledWith(BUCKET, [
+ "file 1",
+ "file 2",
+ ])
+ })
+
+ it("should be able to cleanup a column and not throw when ALL attachments are undefined", async () => {
+ const originalTable = tableGenerator()
+ delete originalTable.schema["attach"]
+ await AttachmentCleanup.tableUpdate(
+ originalTable,
+ [{}, { attach: undefined }],
+ {
+ oldTable: tableGenerator(),
+ }
+ )
+ expect(mockedDeleteFiles).not.toHaveBeenCalled()
+ })
+ }
+)
diff --git a/packages/server/src/utilities/rowProcessor/tests/outputProcessing.spec.ts b/packages/server/src/utilities/rowProcessor/tests/outputProcessing.spec.ts
index a17bd5f393..74d55aff36 100644
--- a/packages/server/src/utilities/rowProcessor/tests/outputProcessing.spec.ts
+++ b/packages/server/src/utilities/rowProcessor/tests/outputProcessing.spec.ts
@@ -73,7 +73,7 @@ describe("rowProcessor - outputProcessing", () => {
)
})
- it("should handle attachments correctly", async () => {
+ it("should handle attachment list correctly", async () => {
const table: Table = {
_id: generator.guid(),
name: "TestTable",
@@ -82,7 +82,7 @@ describe("rowProcessor - outputProcessing", () => {
sourceType: TableSourceType.INTERNAL,
schema: {
attach: {
- type: FieldType.ATTACHMENT,
+ type: FieldType.ATTACHMENTS,
name: "attach",
constraints: {},
},
@@ -116,6 +116,47 @@ describe("rowProcessor - outputProcessing", () => {
expect(output3.attach[0].url).toBe("aaaa")
})
+ it("should handle single attachment correctly", async () => {
+ const table: Table = {
+ _id: generator.guid(),
+ name: "TestTable",
+ type: "table",
+ sourceId: INTERNAL_TABLE_SOURCE_ID,
+ sourceType: TableSourceType.INTERNAL,
+ schema: {
+ attach: {
+ type: FieldType.ATTACHMENT_SINGLE,
+ name: "attach",
+ constraints: {},
+ },
+ },
+ }
+
+ const row: { attach: RowAttachment } = {
+ attach: {
+ size: 10,
+ name: "test",
+ extension: "jpg",
+ key: "test.jpg",
+ },
+ }
+
+ const output = await outputProcessing(table, row, { squash: false })
+ expect(output.attach.url).toBe(
+ "/files/signed/prod-budi-app-assets/test.jpg"
+ )
+
+ row.attach.url = ""
+ const output2 = await outputProcessing(table, row, { squash: false })
+ expect(output2.attach.url).toBe(
+ "/files/signed/prod-budi-app-assets/test.jpg"
+ )
+
+ row.attach.url = "aaaa"
+ const output3 = await outputProcessing(table, row, { squash: false })
+ expect(output3.attach.url).toBe("aaaa")
+ })
+
it("process output even when the field is not empty", async () => {
const table: Table = {
_id: generator.guid(),
diff --git a/packages/server/src/utilities/schema.ts b/packages/server/src/utilities/schema.ts
index 34113759ed..4c7f0b7423 100644
--- a/packages/server/src/utilities/schema.ts
+++ b/packages/server/src/utilities/schema.ts
@@ -147,6 +147,12 @@ export function parse(rows: Rows, schema: TableSchema): Rows {
utils.unreachable(columnSubtype)
}
}
+ } else if (
+ (columnType === FieldType.ATTACHMENTS ||
+ columnType === FieldType.ATTACHMENT_SINGLE) &&
+ typeof columnData === "string"
+ ) {
+ parsedRow[columnName] = parseCsvExport(columnData)
} else {
parsedRow[columnName] = columnData
}
diff --git a/packages/shared-core/src/table.ts b/packages/shared-core/src/table.ts
index 5eab2fc340..26a7e77cd0 100644
--- a/packages/shared-core/src/table.ts
+++ b/packages/shared-core/src/table.ts
@@ -11,10 +11,10 @@ const allowDisplayColumnByType: Record = {
[FieldType.INTERNAL]: true,
[FieldType.BARCODEQR]: true,
[FieldType.BIGINT]: true,
-
[FieldType.BOOLEAN]: false,
[FieldType.ARRAY]: false,
- [FieldType.ATTACHMENT]: false,
+ [FieldType.ATTACHMENTS]: false,
+ [FieldType.ATTACHMENT_SINGLE]: false,
[FieldType.LINK]: false,
[FieldType.JSON]: false,
[FieldType.BB_REFERENCE]: false,
@@ -34,7 +34,8 @@ const allowSortColumnByType: Record = {
[FieldType.JSON]: true,
[FieldType.FORMULA]: false,
- [FieldType.ATTACHMENT]: false,
+ [FieldType.ATTACHMENTS]: false,
+ [FieldType.ATTACHMENT_SINGLE]: false,
[FieldType.ARRAY]: false,
[FieldType.LINK]: false,
[FieldType.BB_REFERENCE]: false,
diff --git a/packages/types/src/documents/app/row.ts b/packages/types/src/documents/app/row.ts
index aa8f50d4a8..222c346591 100644
--- a/packages/types/src/documents/app/row.ts
+++ b/packages/types/src/documents/app/row.ts
@@ -8,7 +8,8 @@ export enum FieldType {
BOOLEAN = "boolean",
ARRAY = "array",
DATETIME = "datetime",
- ATTACHMENT = "attachment",
+ ATTACHMENTS = "attachment",
+ ATTACHMENT_SINGLE = "attachment_single",
LINK = "link",
FORMULA = "formula",
AUTO = "auto",
@@ -38,7 +39,6 @@ export interface Row extends Document {
export enum FieldSubtype {
USER = "user",
USERS = "users",
- SINGLE = "single",
}
// The 'as' are required for typescript not to type the outputs as generic FieldSubtype
@@ -47,7 +47,4 @@ export const FieldTypeSubtypes = {
USER: FieldSubtype.USER as FieldSubtype.USER,
USERS: FieldSubtype.USERS as FieldSubtype.USERS,
},
- ATTACHMENT: {
- SINGLE: FieldSubtype.SINGLE as FieldSubtype.SINGLE,
- },
}
diff --git a/packages/types/src/documents/app/table/schema.ts b/packages/types/src/documents/app/table/schema.ts
index 45e39268ac..86c34b6a5c 100644
--- a/packages/types/src/documents/app/table/schema.ts
+++ b/packages/types/src/documents/app/table/schema.ts
@@ -112,10 +112,8 @@ export interface BBReferenceFieldMetadata
relationshipType?: RelationshipType
}
-export interface AttachmentFieldMetadata
- extends Omit {
- type: FieldType.ATTACHMENT
- subtype?: FieldSubtype.SINGLE
+export interface AttachmentFieldMetadata extends BaseFieldSchema {
+ type: FieldType.ATTACHMENTS
}
export interface FieldConstraints {
@@ -164,7 +162,7 @@ interface OtherFieldMetadata extends BaseFieldSchema {
| FieldType.NUMBER
| FieldType.LONGFORM
| FieldType.BB_REFERENCE
- | FieldType.ATTACHMENT
+ | FieldType.ATTACHMENTS
>
}
@@ -217,5 +215,5 @@ export function isBBReferenceField(
export function isAttachmentField(
field: FieldSchema
): field is AttachmentFieldMetadata {
- return field.type === FieldType.ATTACHMENT
+ return field.type === FieldType.ATTACHMENTS
}