UNPKG

nylas

Version:

A NodeJS wrapper for the Nylas REST API for email, contacts, and calendar.

180 lines (179 loc) 7.06 kB
import fetch, { Request } from 'node-fetch'; import { NylasApiError, NylasOAuthError, NylasSdkTimeoutError, } from './models/error.js'; import { objKeysToCamelCase, objKeysToSnakeCase } from './utils.js'; import { SDK_VERSION } from './version.js'; import { snakeCase } from 'change-case'; /** * The header key for the debugging flow ID */ export const FLOW_ID_HEADER = 'x-fastly-id'; /** * The header key for the request ID */ export const REQUEST_ID_HEADER = 'x-request-id'; /** * The API client for communicating with the Nylas API * @ignore Not for public use */ export default class APIClient { constructor({ apiKey, apiUri, timeout, headers }) { this.apiKey = apiKey; this.serverUrl = apiUri; this.timeout = timeout * 1000; // fetch timeout uses milliseconds this.headers = headers; } setRequestUrl({ overrides, path, queryParams, }) { const url = new URL(`${overrides?.apiUri || this.serverUrl}${path}`); return this.setQueryStrings(url, queryParams); } setQueryStrings(url, queryParams) { if (queryParams) { for (const [key, value] of Object.entries(queryParams)) { const snakeCaseKey = snakeCase(key); if (key == 'metadataPair') { // The API understands a metadata_pair filter in the form of: // <key>:<value> const metadataPair = []; for (const item in value) { metadataPair.push(`${item}:${value[item]}`); } url.searchParams.set('metadata_pair', metadataPair.join(',')); } else if (Array.isArray(value)) { for (const item of value) { url.searchParams.append(snakeCaseKey, item); } } else if (typeof value === 'object') { for (const item in value) { url.searchParams.append(snakeCaseKey, `${item}:${value[item]}`); } } else { url.searchParams.set(snakeCaseKey, value); } } } return url; } setRequestHeaders({ headers, overrides, }) { const mergedHeaders = { ...headers, ...this.headers, ...overrides?.headers, }; return { Accept: 'application/json', 'User-Agent': `Nylas Node SDK v${SDK_VERSION}`, Authorization: `Bearer ${overrides?.apiKey || this.apiKey}`, ...mergedHeaders, }; } async sendRequest(options) { const req = this.newRequest(options); const controller = new AbortController(); const timeoutDuration = options.overrides?.timeout || this.timeout; const timeout = setTimeout(() => { controller.abort(); }, timeoutDuration); try { const response = await fetch(req, { signal: controller.signal, }); clearTimeout(timeout); if (typeof response === 'undefined') { throw new Error('Failed to fetch response'); } const headers = response?.headers?.entries ? Object.fromEntries(response.headers.entries()) : {}; const flowId = headers[FLOW_ID_HEADER]; const requestId = headers[REQUEST_ID_HEADER]; if (response.status > 299) { const text = await response.text(); let 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, requestId, flowId, headers); } else { error = new NylasApiError(camelCaseError, response.status, requestId, flowId, headers); } } catch (e) { throw new Error(`Received an error but could not parse response from the server${flowId ? ` with flow ID ${flowId}` : ''}: ${text}`); } throw error; } return response; } catch (error) { if (error instanceof Error && error.name === 'AbortError') { throw new NylasSdkTimeoutError(req.url, this.timeout); } clearTimeout(timeout); throw error; } } requestOptions(optionParams) { const requestOptions = {}; requestOptions.url = this.setRequestUrl(optionParams); requestOptions.headers = this.setRequestHeaders(optionParams); requestOptions.method = optionParams.method; if (optionParams.body) { requestOptions.body = JSON.stringify(objKeysToSnakeCase(optionParams.body, ['metadata']) // metadata should remain as is ); requestOptions.headers['Content-Type'] = 'application/json'; } if (optionParams.form) { requestOptions.body = optionParams.form; requestOptions.headers = { ...requestOptions.headers, ...optionParams.form.getHeaders(), }; } return requestOptions; } newRequest(options) { const newOptions = this.requestOptions(options); return new Request(newOptions.url, { method: newOptions.method, headers: newOptions.headers, body: newOptions.body, }); } async requestWithResponse(response) { const headers = response?.headers?.entries ? Object.fromEntries(response.headers.entries()) : {}; const flowId = headers[FLOW_ID_HEADER]; const text = await response.text(); try { const responseJSON = JSON.parse(text); // Inject the flow ID and headers into the response responseJSON.flowId = flowId; responseJSON.headers = headers; return objKeysToCamelCase(responseJSON, ['metadata']); } catch (e) { throw new Error(`Could not parse response from the server: ${text}`); } } async request(options) { const response = await this.sendRequest(options); return this.requestWithResponse(response); } async requestRaw(options) { const response = await this.sendRequest(options); return response.buffer(); } async requestStream(options) { const response = await this.sendRequest(options); return response.body; } }