Skip to content

Commit

Permalink
fix: Replace form-data with formdata-node
Browse files Browse the repository at this point in the history
- Remove form-data package dependency
- Add formdata-node and form-data-encoder packages
- Update code to use FormData from formdata-node
- Update test mocks to use formdata-node
- Handle ReadableStream attachments properly

Fixes #609
  • Loading branch information
devin-ai-integration[bot] committed Dec 24, 2024
1 parent 5a0800b commit 3331d6d
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 62 deletions.
59 changes: 35 additions & 24 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand Down
8 changes: 5 additions & 3 deletions src/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -26,7 +27,7 @@ export interface RequestOptionsParams {
headers?: Record<string, string>;
queryParams?: Record<string, any>;
body?: any;
form?: FormData;
form?: FormData | any; // Support both formdata-node and legacy form-data types
overrides?: OverridableNylasConfig;
}

Expand Down Expand Up @@ -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
};
}

Expand Down
44 changes: 29 additions & 15 deletions src/resources/messages.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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<FormData> {
const form = new FormData();

// Split out the message payload from the attachments
const messagePayload = {
Expand All @@ -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;
}
Expand Down
26 changes: 16 additions & 10 deletions tests/resources/drafts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any> = {};

this.append = (key: string, value: any): void => {
appendedData[key] = value;
};

this._getAppendedData = (): Record<string, any> => appendedData;
});
jest.mock('formdata-node', () => {
return {
FormData: jest.fn().mockImplementation(function(this: MockedFormData) {
const appendedData: Record<string, any> = {};
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', () => {
Expand Down
18 changes: 10 additions & 8 deletions tests/resources/messages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any> = {};
jest.mock('formdata-node', () => {
return {
FormData: jest.fn().mockImplementation(function(this: MockedFormData) {
const appendedData: Record<string, any> = {};

this.append = (key: string, value: any): void => {
appendedData[key] = value;
};
this.append = (key: string, value: any): void => {
appendedData[key] = value;
};

this._getAppendedData = (): Record<string, any> => appendedData;
});
this._getAppendedData = (): Record<string, any> => appendedData;
})
};
});

describe('Messages', () => {
Expand Down

0 comments on commit 3331d6d

Please sign in to comment.