Skip to content

Commit

Permalink
Add support for attachments api (#505)
Browse files Browse the repository at this point in the history
* Attachments api

* Added download method for returning stream

* lint

* PR Review request updates
  • Loading branch information
yifanplanet authored Dec 21, 2023
1 parent 38f075c commit f9f18e5
Show file tree
Hide file tree
Showing 7 changed files with 345 additions and 39 deletions.
100 changes: 61 additions & 39 deletions src/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,55 @@ export default class APIClient {
};
}

private async sendRequest(options: RequestOptionsParams): Promise<Response> {
const req = this.newRequest(options);
const controller: AbortController = new AbortController();
const timeout = setTimeout(() => {
controller.abort();
throw new NylasSdkTimeoutError(req.url, this.timeout);
}, this.timeout);

try {
const response = await fetch(req, { signal: controller.signal });
clearTimeout(timeout);

if (typeof response === 'undefined') {
throw new Error('Failed to fetch response');
}

if (response.status > 299) {
const text = await response.text();
let error: Error;
try {
const parsedError = JSON.parse(text);
const camelCaseError = objKeysToCamelCase(parsedError);

// Check if the request is an authentication request
const isAuthRequest =
options.path.includes('connect/token') ||
options.path.includes('connect/revoke');

if (isAuthRequest) {
error = new NylasOAuthError(camelCaseError, response.status);
} else {
error = new NylasApiError(camelCaseError, response.status);
}
} catch (e) {
throw new Error(
`Received an error but could not parse response from the server: ${text}`
);
}

throw error;
}

return response;
} catch (error) {
clearTimeout(timeout);
throw error;
}
}

requestOptions(optionParams: RequestOptionsParams): RequestOptions {
const requestOptions = {} as RequestOptions;

Expand Down Expand Up @@ -166,46 +215,19 @@ export default class APIClient {
}

async request<T>(options: RequestOptionsParams): Promise<T> {
const req = this.newRequest(options);
const controller: AbortController = new AbortController();
const timeout = setTimeout(() => {
controller.abort();
throw new NylasSdkTimeoutError(req.url, this.timeout);
}, this.timeout);

const response = await fetch(req, { signal: controller.signal });
clearTimeout(timeout);

if (typeof response === 'undefined') {
throw new Error('Failed to fetch response');
}

// handle error response
if (response.status > 299) {
const authErrorResponse =
options.path.includes('connect/token') ||
options.path.includes('connect/revoke');

const text = await response.text();
let error: Error;
try {
const parsedError = JSON.parse(text);
const camelCaseError = objKeysToCamelCase(parsedError);

if (authErrorResponse) {
error = new NylasOAuthError(camelCaseError, response.status);
} else {
error = new NylasApiError(camelCaseError, response.status);
}
} catch (e) {
throw new Error(
`Received an error but could not parse response from the server: ${text}`
);
}
const response = await this.sendRequest(options);
return this.requestWithResponse(response);
}

throw error;
}
async requestRaw(options: RequestOptionsParams): Promise<Buffer> {
const response = await this.sendRequest(options);
return response.buffer();
}

return this.requestWithResponse(response);
async requestStream(
options: RequestOptionsParams
): Promise<NodeJS.ReadableStream> {
const response = await this.sendRequest(options);
return response.body;
}
}
49 changes: 49 additions & 0 deletions src/models/attachments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Interface of an attachment object from Nylas.
*/
export interface Attachment {
/**
* A globally unique object identifier.
*/
id: string;

/**
* Attachment's name.
*/
filename: string;

/**
* Attachment's content type.
*/
contentType: string;

/**
* Grant ID of the Nylas account.
*/
grantId: string;

/**
* If it's an inline attachment.
*/
isInline: boolean;

/**
* Attachment's size in bytes.
*/
size: number;
}

/**
* Interface representing of the query parameters for finding an attachment's metadata.
*/
export interface FindAttachmentQueryParams {
/**
* ID of the message the attachment belongs to.
*/
messageId: string;
}

/**
* Interface representing of the query parameters for downloading an attachment.
*/
export type DownloadAttachmentQueryParams = FindAttachmentQueryParams;
6 changes: 6 additions & 0 deletions src/nylas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Drafts } from './resources/drafts.js';
import { Threads } from './resources/threads.js';
import { Connectors } from './resources/connectors.js';
import { Folders } from './resources/folders.js';
import { Attachments } from './resources/attachments.js';

/**
* The entry point to the Node SDK
Expand Down Expand Up @@ -58,6 +59,10 @@ export default class Nylas {
* Access the Folders API
*/
public folders: Folders;
/**
* Access the Attachments API
*/
public attachments: Attachments;

/**
* The configured API client
Expand Down Expand Up @@ -85,6 +90,7 @@ export default class Nylas {
this.threads = new Threads(this.apiClient);
this.webhooks = new Webhooks(this.apiClient);
this.folders = new Folders(this.apiClient);
this.attachments = new Attachments(this.apiClient);

return this;
}
Expand Down
100 changes: 100 additions & 0 deletions src/resources/attachments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { Overrides } from '../config.js';
import {
Attachment,
FindAttachmentQueryParams,
DownloadAttachmentQueryParams,
} from '../models/attachments.js';
import { NylasResponse } from '../models/response.js';
import { Resource } from './resource.js';

/**
* @property identifier The ID of the grant to act upon. Use "me" to refer to the grant associated with an access token.
* @property attachmentId The ID of the attachment to act upon.
* @property queryParams The query parameters to include in the request
*/
interface FindAttachmentParams {
identifier: string;
attachmentId: string;
queryParams: FindAttachmentQueryParams;
}

/**
* @property identifier The ID of the grant to act upon. Use "me" to refer to the grant associated with an access token.
* @property attachmentId The ID of the attachment to act upon.
* @property queryParams The query parameters to include in the request
*/
interface DownloadAttachmentParams {
identifier: string;
attachmentId: string;
queryParams: DownloadAttachmentQueryParams;
}

/**
* Nylas Attachments API
*
* The Nylas Attachments API allows you to retrieve metadata and download attachments.
*/
export class Attachments extends Resource {
/**
* Returns an attachment by ID.
* @return The Attachment metadata
*/
public find({
identifier,
attachmentId,
queryParams,
overrides,
}: FindAttachmentParams & Overrides): Promise<NylasResponse<Attachment>> {
return super._find({
path: `/v3/grants/${identifier}/attachments/${attachmentId}`,
queryParams,
overrides,
});
}

/**
* Download the attachment data
*
* This method returns a NodeJS.ReadableStream which can be used to stream the attachment data.
* This is particularly useful for handling large attachments efficiently, as it avoids loading
* the entire file into memory. The stream can be piped to a file stream or used in any other way
* that Node.js streams are typically used.
*
* @param identifier Grant ID or email account to query
* @param attachmentId The id of the attachment to download.
* @param queryParams The query parameters to include in the request
* @returns {NodeJS.ReadableStream} The ReadableStream containing the file data.
*/
public download({
identifier,
attachmentId,
queryParams,
overrides,
}: DownloadAttachmentParams & Overrides): Promise<NodeJS.ReadableStream> {
return this._getStream({
path: `/v3/grants/${identifier}/attachments/${attachmentId}/download`,
queryParams,
overrides,
});
}

/**
* Download the attachment as a byte array
* @param identifier Grant ID or email account to query
* @param attachmentId The id of the attachment to download.
* @param queryParams The query parameters to include in the request
* @return The raw file data
*/
public downloadBytes({
identifier,
attachmentId,
queryParams,
overrides,
}: DownloadAttachmentParams & Overrides): Promise<Buffer> {
return super._getRaw({
path: `/v3/grants/${identifier}/attachments/${attachmentId}/download`,
queryParams,
overrides,
});
}
}
26 changes: 26 additions & 0 deletions src/resources/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,32 @@ export class Resource {
overrides,
});
}

protected _getRaw({
path,
queryParams,
overrides,
}: FindParams<void>): Promise<Buffer> {
return this.apiClient.requestRaw({
method: 'GET',
path,
queryParams,
overrides,
});
}

protected _getStream({
path,
queryParams,
overrides,
}: FindParams<void>): Promise<NodeJS.ReadableStream> {
return this.apiClient.requestStream({
method: 'GET',
path,
queryParams,
overrides,
});
}
}

type ListYieldReturn<T> = T & {
Expand Down
1 change: 1 addition & 0 deletions tests/nylas.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe('Nylas', () => {
expect(nylas.events.constructor.name).toBe('Events');
expect(nylas.webhooks.constructor.name).toBe('Webhooks');
expect(nylas.folders.constructor.name).toBe('Folders');
expect(nylas.attachments.constructor.name).toBe('Attachments');
});

it('should configure the apiClient', () => {
Expand Down
Loading

0 comments on commit f9f18e5

Please sign in to comment.