diff --git a/package-lock.json b/package-lock.json index 78ce4021..88e6a7e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,8 @@ "license": "MIT", "dependencies": { "change-case": "^4.1.2", - "eslint-plugin-import": "^2.28.1", - "form-data": "^4.0.0", + "form-data-encoder": "^4.0.2", + "formdata-node": "^6.0.3", "mime-types": "^2.1.35", "node-fetch": "^2.6.12", "uuid": "^8.3.2" @@ -1913,7 +1913,8 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true }, "node_modules/available-typed-arrays": { "version": "1.0.5", @@ -2425,6 +2426,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -2545,6 +2547,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, "engines": { "node": ">=0.4.0" } @@ -3351,17 +3354,22 @@ "is-callable": "^1.1.3" } }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, + "node_modules/form-data-encoder": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.0.2.tgz", + "integrity": "sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==", + "license": "MIT", "engines": { - "node": ">= 6" + "node": ">= 18" + } + }, + "node_modules/formdata-node": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-6.0.3.tgz", + "integrity": "sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==", + "license": "MIT", + "engines": { + "node": ">= 18" } }, "node_modules/fs.realpath": { @@ -9437,7 +9445,8 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true }, "available-typed-arrays": { "version": "1.0.5", @@ -9819,6 +9828,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -9914,7 +9924,8 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true }, "detect-newline": { "version": "3.1.0", @@ -10546,15 +10557,15 @@ "is-callable": "^1.1.3" } }, - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } + "form-data-encoder": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.0.2.tgz", + "integrity": "sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==" + }, + "formdata-node": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-6.0.3.tgz", + "integrity": "sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==" }, "fs.realpath": { "version": "1.0.0", diff --git a/package.json b/package.json index 7abb47f0..001f50f6 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "license": "MIT", "dependencies": { "change-case": "^4.1.2", - "form-data": "^4.0.0", + "form-data-encoder": "^4.0.2", + "formdata-node": "^6.0.3", "mime-types": "^2.1.35", "node-fetch": "^2.6.12", "uuid": "^8.3.2" @@ -56,8 +57,8 @@ "eslint": "^5.14.0", "eslint-config-prettier": "^4.0.0", "eslint-plugin-custom-rules": "^0.0.0", - "eslint-plugin-prettier": "^3.0.1", "eslint-plugin-import": "^2.28.1", + "eslint-plugin-prettier": "^3.0.1", "jest": "^29.6.1", "prettier": "^1.19.1", "ts-jest": "^29.1.1", diff --git a/src/apiClient.ts b/src/apiClient.ts index 1e118b3b..3db04ca6 100644 --- a/src/apiClient.ts +++ b/src/apiClient.ts @@ -7,7 +7,8 @@ import { } from './models/error.js'; import { objKeysToCamelCase, objKeysToSnakeCase } from './utils.js'; import { SDK_VERSION } from './version.js'; -import FormData from 'form-data'; +import { FormData } from 'formdata-node'; +import { FormDataEncoder } from 'form-data-encoder'; import { snakeCase } from 'change-case'; /** @@ -26,7 +27,7 @@ export interface RequestOptionsParams { headers?: Record; queryParams?: Record; body?: any; - form?: FormData; + form?: FormData | any; // Support both formdata-node and legacy form-data types overrides?: OverridableNylasConfig; } @@ -209,9 +210,10 @@ export default class APIClient { if (optionParams.form) { requestOptions.body = optionParams.form; + const encoder = new FormDataEncoder(optionParams.form); requestOptions.headers = { ...requestOptions.headers, - ...optionParams.form.getHeaders(), + 'content-type': encoder.contentType }; } diff --git a/src/resources/messages.ts b/src/resources/messages.ts index 734b0f50..8cbc1e91 100644 --- a/src/resources/messages.ts +++ b/src/resources/messages.ts @@ -1,4 +1,4 @@ -import FormData from 'form-data'; +import { FormData, File, Blob } from 'formdata-node'; import APIClient, { RequestOptionsParams } from '../apiClient.js'; import { Overrides } from '../config.js'; import { @@ -220,7 +220,7 @@ export class Messages extends Resource { }, 0) || 0; if (attachmentSize >= Messages.MAXIMUM_JSON_ATTACHMENT_SIZE) { - requestOptions.form = Messages._buildFormRequest(requestBody); + requestOptions.form = await Messages._buildFormRequest(requestBody); } else { if (requestBody.attachments) { const processedAttachments = await encodeAttachmentStreams( @@ -308,13 +308,10 @@ export class Messages extends Resource { }); } - static _buildFormRequest( + static async _buildFormRequest( requestBody: CreateDraftRequest | UpdateDraftRequest | SendMessageRequest - ): FormData { - // FormData imports are funky, cjs needs to use .default, es6 doesn't - const FD = require('form-data'); - const FormDataConstructor = FD.default || FD; - const form: FormData = new FormDataConstructor(); + ): Promise { + const form = new FormData(); // Split out the message payload from the attachments const messagePayload = { @@ -324,13 +321,30 @@ export class Messages extends Resource { form.append('message', JSON.stringify(objKeysToSnakeCase(messagePayload))); // Add a separate form field for each attachment - requestBody.attachments?.forEach((attachment, index) => { - const contentId = attachment.contentId || `file${index}`; - form.append(contentId, attachment.content, { - filename: attachment.filename, - contentType: attachment.contentType, - }); - }); + if (requestBody.attachments) { + for (const [index, attachment] of requestBody.attachments.entries()) { + const contentId = attachment.contentId || `file${index}`; + // Handle different types of content (Buffer, ReadableStream, string) + let file; + if (attachment.content instanceof Buffer || typeof attachment.content === 'string') { + file = new File([attachment.content], attachment.filename, { type: attachment.contentType }); + } else if (attachment.content instanceof ReadableStream) { + // For ReadableStream, we need to read it into a buffer first + const chunks: Buffer[] = []; + const reader = attachment.content.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(Buffer.from(value)); + } + const buffer = Buffer.concat(chunks); + file = new File([buffer], attachment.filename, { type: attachment.contentType }); + } else { + throw new Error('Unsupported attachment content type'); + } + form.append(contentId, file); + } + } return form; } diff --git a/tests/resources/drafts.spec.ts b/tests/resources/drafts.spec.ts index 47ea8f4e..a7087f74 100644 --- a/tests/resources/drafts.spec.ts +++ b/tests/resources/drafts.spec.ts @@ -6,16 +6,22 @@ import { createReadableStream, MockedFormData } from '../testUtils'; jest.mock('../src/apiClient'); // Mock the FormData constructor -jest.mock('form-data', () => { - return jest.fn().mockImplementation(function(this: MockedFormData) { - const appendedData: Record = {}; - - this.append = (key: string, value: any): void => { - appendedData[key] = value; - }; - - this._getAppendedData = (): Record => appendedData; - }); +jest.mock('formdata-node', () => { + return { + FormData: jest.fn().mockImplementation(function(this: MockedFormData) { + const appendedData: Record = {}; + this.append = (key: string, value: any) => { + appendedData[key] = value; + }; + this._getAppendedData = () => appendedData; + return this; + }), + File: jest.fn().mockImplementation((content: any[], name: string) => ({ + content, + name, + })), + Blob: jest.fn(), + }; }); describe('Drafts', () => { diff --git a/tests/resources/messages.spec.ts b/tests/resources/messages.spec.ts index 20393683..9427b92c 100644 --- a/tests/resources/messages.spec.ts +++ b/tests/resources/messages.spec.ts @@ -5,16 +5,18 @@ import { CreateAttachmentRequest } from '../../src/models/attachments'; jest.mock('../src/apiClient'); // Mock the FormData constructor -jest.mock('form-data', () => { - return jest.fn().mockImplementation(function(this: MockedFormData) { - const appendedData: Record = {}; +jest.mock('formdata-node', () => { + return { + FormData: jest.fn().mockImplementation(function(this: MockedFormData) { + const appendedData: Record = {}; - this.append = (key: string, value: any): void => { - appendedData[key] = value; - }; + this.append = (key: string, value: any): void => { + appendedData[key] = value; + }; - this._getAppendedData = (): Record => appendedData; - }); + this._getAppendedData = (): Record => appendedData; + }) + }; }); describe('Messages', () => {