diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 158b36092c..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 = { + const singleAttachment: FieldSchema = { + type: FieldType.ATTACHMENT_SINGLE, + name: "single attachment", + constraints: { presence: false }, + } + const attachmentList: AttachmentFieldMetadata = { type: FieldType.ATTACHMENTS, - name: "attachment", + 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,7 +831,39 @@ 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_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: { 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 8fd2ccf795..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.ATTACHMENTS) { + if ( + column.type === FieldType.ATTACHMENTS || + column.type === FieldType.ATTACHMENT_SINGLE + ) { attachmentCols.push(key) } } diff --git a/packages/server/src/sdk/tests/attachments.spec.ts b/packages/server/src/sdk/tests/attachments.spec.ts index 3ae9a33d8b..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.ATTACHMENTS, + 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/tests/outputProcessing.spec.ts b/packages/server/src/utilities/rowProcessor/tests/outputProcessing.spec.ts index 93404e0469..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", @@ -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(),