diff --git a/lib/utils.ts b/lib/utils.ts index e65ebc95..d3e29903 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -13,17 +13,35 @@ export function ensureJSON(raw: T): T { } } +function toArrayBuffer(input: Uint8Array | Buffer): ArrayBuffer { + if (input.buffer instanceof ArrayBuffer) { + return input.buffer.slice( + input.byteOffset, + input.byteOffset + input.byteLength, + ); + } + const arrayBuffer = new ArrayBuffer(input.byteLength); + new Uint8Array(arrayBuffer).set(input); + return arrayBuffer; +} + export function createMultipartFormData( this: FormData | void, formBody: Record, ): FormData { const formData = this instanceof FormData ? this : new FormData(); - Object.entries(formBody).forEach(([key, value]) => { - if (Buffer.isBuffer(value) || value instanceof Uint8Array) { - formData.append(key, new Blob([value])); + for (const [key, value] of Object.entries(formBody)) { + if (value == null) continue; + + if (value instanceof Blob) { + formData.append(key, value); + } else if (Buffer.isBuffer(value) || value instanceof Uint8Array) { + const arrayBuffer = toArrayBuffer(value); + formData.append(key, new Blob([arrayBuffer])); } else { formData.append(key, String(value)); } - }); + } + return formData; } diff --git a/package-lock.json b/package-lock.json index 4a211f4a..b8139ad4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,7 @@ "version": "1.0.0-test", "license": "Apache-2.0", "dependencies": { - "@types/node": "^22.0.0", - "axios": "^1.12.0" + "@types/node": "^22.0.0" }, "devDependencies": { "@types/express": "5.0.3", @@ -5153,9 +5152,9 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/test/client.spec.ts b/test/client.spec.ts index 89dccdf7..6b5b1e5c 100644 --- a/test/client.spec.ts +++ b/test/client.spec.ts @@ -1,7 +1,13 @@ import { readFileSync } from "node:fs"; import { Buffer } from "node:buffer"; import { join } from "node:path"; -import { deepEqual, equal, ok, strictEqual } from "node:assert"; +import { + deepEqual, + deepStrictEqual, + equal, + ok, + strictEqual, +} from "node:assert"; import { URL } from "node:url"; import Client, { OAuth } from "../lib/client.js"; import * as Types from "../lib/types.js"; @@ -17,6 +23,7 @@ import { import { describe, it, beforeAll, afterAll, afterEach } from "vitest"; import { parseForm } from "./helpers/parse-form"; +import { Readable } from "node:stream"; const channelAccessToken = "test_channel_access_token"; @@ -994,7 +1001,7 @@ describe("client", () => { equal(scope.isDone(), true); }); - it("createUploadAudienceGroupByFile", async () => { + it("createUploadAudienceGroupByFile with buffer", async () => { const filepath = join(__dirname, "/helpers/line-icon.png"); const buffer = readFileSync(filepath); @@ -1017,20 +1024,23 @@ describe("client", () => { .startsWith(`multipart/form-data; boundary=`), ); - const blob = await request.blob(); - const arrayBuffer = await blob.arrayBuffer(); - const formData = parseForm(arrayBuffer); - equal(formData["description"], requestBody.description); + const formData = await request.formData(); + equal(formData.get("description"), requestBody.description); equal( - formData["isIfaAudience"], + formData.get("isIfaAudience"), requestBody.isIfaAudience.toString(), ); - equal(formData["uploadDescription"], requestBody.uploadDescription); equal( - Buffer.from(await (formData["file"] as Blob).arrayBuffer()), - requestBody.file.toString(), + formData.get("uploadDescription"), + requestBody.uploadDescription, ); + const filePart = formData.get("file"); + ok(filePart instanceof Blob); + const sent = Buffer.from(await (filePart as Blob).arrayBuffer()); + + deepStrictEqual(sent, requestBody.file); + scope.done(); return HttpResponse.json({}); }, @@ -1041,6 +1051,57 @@ describe("client", () => { equal(scope.isDone(), true); }); + it("createUploadAudienceGroupByFile with Readable stream", async () => { + const filepath = join(__dirname, "/helpers/line-icon.png"); + const buffer = readFileSync(filepath); + + const requestBody = { + description: "audienceGroupName", + isIfaAudience: true, + uploadDescription: "uploadDescription", + file: Readable.from(buffer), + }; + + const scope = new MSWResult(); + server.use( + http.post( + DATA_API_PREFIX + "/audienceGroup/upload/byFile", + async ({ request }) => { + checkInterceptionOption(request, interceptionOption); + ok( + request.headers + .get("content-type") + .startsWith("multipart/form-data; boundary="), + ); + + const formData = await request.formData(); + + equal(formData.get("description"), requestBody.description); + equal( + formData.get("isIfaAudience"), + requestBody.isIfaAudience.toString(), + ); + equal( + formData.get("uploadDescription"), + requestBody.uploadDescription, + ); + + const filePart = formData.get("file"); + ok(filePart instanceof Blob); + + const sent = Buffer.from(await (filePart as Blob).arrayBuffer()); + deepStrictEqual(sent, buffer); + + scope.done(); + return HttpResponse.json({}); + }, + ), + ); + + await client.createUploadAudienceGroupByFile(requestBody as any); + equal(scope.isDone(), true); + }); + it("updateUploadAudienceGroup", async () => { const requestBody = { audienceGroupId: 4389303728991,