diff --git a/.changeset/fluffy-horses-enjoy.md b/.changeset/fluffy-horses-enjoy.md new file mode 100644 index 000000000..9ed297817 --- /dev/null +++ b/.changeset/fluffy-horses-enjoy.md @@ -0,0 +1,6 @@ +--- +"@osdk/client": patch +"@osdk/api": patch +--- + +Fixes attachment upload inputs sending incorrect in browser contexts diff --git a/etc/api.report.api.md b/etc/api.report.api.md index 9552df3ad..91dc8b809 100644 --- a/etc/api.report.api.md +++ b/etc/api.report.api.md @@ -204,8 +204,10 @@ export interface Attachment { rid: string; } -// @public -export interface AttachmentUpload extends Blob { +// @public (undocumented) +export interface AttachmentUpload { + // (undocumented) + readonly data: Blob; // (undocumented) readonly name: string; } @@ -243,7 +245,9 @@ export type ConvertProps; } -/** - * This interface should also accept the File object from - * the W3C FileApi https://www.w3.org/TR/FileAPI/#file-section - */ -export interface AttachmentUpload extends Blob { + +export interface AttachmentUpload { readonly name: string; + readonly data: Blob; } export interface AttachmentMetadata { diff --git a/packages/client/src/actions/actions.test.ts b/packages/client/src/actions/actions.test.ts index a01f37e9c..d4bf51416 100644 --- a/packages/client/src/actions/actions.test.ts +++ b/packages/client/src/actions/actions.test.ts @@ -217,10 +217,24 @@ describe("actions", () => { typeof clientBoundBatchActionTakesAttachment >[0]; - expectTypeOf<{ attachment: string | AttachmentUpload }>().toMatchTypeOf< + expectTypeOf< + { + attachment: + | string + | AttachmentUpload + | Blob & { readonly name: string }; + } + >().toMatchTypeOf< InferredParamType >(); - expectTypeOf<{ attachment: string | AttachmentUpload }[]>().toMatchTypeOf< + expectTypeOf< + { + attachment: + | string + | AttachmentUpload + | Blob & { readonly name: string }; + }[] + >().toMatchTypeOf< InferredBatchParamType >(); @@ -228,12 +242,23 @@ describe("actions", () => { stubData.attachmentUploadRequestBody[stubData.localAttachment1.filename]; const attachment = createAttachmentUpload(blob, "file1.txt"); + + // Mimics the Web file API (https://developer.mozilla.org/en-US/docs/Web/API/File). The File constructor is only available in Node 19.2.0 and above + const fileAttachment = Object.assign(blob, { name: "file1.txt" }); + const result = await client(actionTakesAttachment).applyAction({ attachment, }); + const result2 = await client(actionTakesAttachment).applyAction({ + attachment: fileAttachment, + }); + expectTypeOf().toEqualTypeOf(); expect(result).toBeUndefined(); + + expectTypeOf().toEqualTypeOf(); + expect(result2).toBeUndefined(); }); it("conditionally returns edits in batch mode", async () => { const result = await client(moveOffice).batchApplyAction([ diff --git a/packages/client/src/object/AttachmentUpload.ts b/packages/client/src/object/AttachmentUpload.ts index 50b778146..9fb4f1a5d 100644 --- a/packages/client/src/object/AttachmentUpload.ts +++ b/packages/client/src/object/AttachmentUpload.ts @@ -17,18 +17,13 @@ import type { AttachmentUpload } from "@osdk/api"; export function isAttachmentUpload(o: any): o is AttachmentUpload { - return o instanceof Blob && "name" in o; + return typeof o === `object` && "name" in o && "data" in o + && o.data instanceof Blob; } export function createAttachmentUpload( data: Blob, name: string, ): AttachmentUpload { - const attachmentUpload = Object.create(data, { - name: { value: name }, - size: { value: data.size }, - type: { value: data.type }, - }); - - return attachmentUpload as AttachmentUpload; + return { data, name }; } diff --git a/packages/client/src/util/toDataValue.test.ts b/packages/client/src/util/toDataValue.test.ts index b2b775bd7..d913315d0 100644 --- a/packages/client/src/util/toDataValue.test.ts +++ b/packages/client/src/util/toDataValue.test.ts @@ -141,7 +141,7 @@ describe(toDataValue, () => { expect(definitionConversion).toMatchInlineSnapshot(expected); }); - it("converts attachment uploads correctly", async () => { + it("converts blob attachment uploads correctly", async () => { const blob = stubData.attachmentUploadRequestBody[stubData.localAttachment1.filename]; const attachmentUpload = createAttachmentUpload(blob, "file1.txt"); @@ -151,4 +151,18 @@ describe(toDataValue, () => { "ri.attachments.main.attachment.86016861-707f-4292-b258-6a7108915a75", ); }); + + it("converts file attachment uploads correctly", async () => { + // Mimics the Web file API (https://developer.mozilla.org/en-US/docs/Web/API/File). The File constructor is only available in Node 19.2.0 and above + const file = Object.assign( + stubData.attachmentUploadRequestBody[stubData.localAttachment1.filename], + { name: "file1.txt" }, + ); + + const converted = await toDataValue(file, clientCtx); + + expect(converted).toEqual( + "ri.attachments.main.attachment.86016861-707f-4292-b258-6a7108915a75", + ); + }); }); diff --git a/packages/client/src/util/toDataValue.ts b/packages/client/src/util/toDataValue.ts index 84f9145cd..b2e2d6402 100644 --- a/packages/client/src/util/toDataValue.ts +++ b/packages/client/src/util/toDataValue.ts @@ -51,7 +51,7 @@ export async function toDataValue( if (isAttachmentUpload(value)) { const attachment = await OntologiesV2.Attachments.upload( client, - value, + value.data, { filename: value.name, }, @@ -59,6 +59,17 @@ export async function toDataValue( return await toDataValue(attachment.rid, client); } + if (typeof value === "object" && value instanceof Blob && "name" in value) { + const attachment = await OntologiesV2.Attachments.upload( + client, + value, + { + filename: value.name as string, + }, + ); + return await toDataValue(attachment.rid, client); + } + // objects just send the JSON'd primaryKey if (isOntologyObjectV2(value)) { return await toDataValue(value.__primaryKey, client); diff --git a/packages/client/src/util/toDataValueQueries.ts b/packages/client/src/util/toDataValueQueries.ts index ae8b4ada7..85a64c71d 100644 --- a/packages/client/src/util/toDataValueQueries.ts +++ b/packages/client/src/util/toDataValueQueries.ts @@ -52,7 +52,7 @@ export async function toDataValueQueries( if (isAttachmentUpload(value)) { const attachment = await OntologiesV2.Attachments.upload( client, - value, + value.data, { filename: value.name, }, @@ -60,6 +60,19 @@ export async function toDataValueQueries( return attachment.rid; } + if ( + typeof value === "object" && value instanceof Blob && "name" in value + ) { + const attachment = await OntologiesV2.Attachments.upload( + client, + value, + { + filename: value.name as string, + }, + ); + return attachment.rid; + } + // If it's not an upload, it's just an attachment rid string which we can pass through return value; } diff --git a/packages/example-generator/src/check.test.ts b/packages/example-generator/src/check.test.ts index 339baf313..2cd765269 100644 --- a/packages/example-generator/src/check.test.ts +++ b/packages/example-generator/src/check.test.ts @@ -21,9 +21,13 @@ import { run } from "./run.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -it("Generates code that matches the files on disk in the examples dir", async () => { - await run({ - outputDirectory: path.join(__dirname, "..", "..", "..", "examples"), - check: true, - }); -}); +it( + "Generates code that matches the files on disk in the examples dir", + async () => { + await run({ + outputDirectory: path.join(__dirname, "..", "..", "..", "examples"), + check: true, + }); + }, + { timeout: 15000 }, +);