@ringcentral/sdk
Version:
- [Installation](#installation) - [Getting Started](#getting-started) - [API Calls](#api-calls) - [Advanced SDK Configuration & Polyfills](#advanced-sdk-configuration--polyfills) - [Making telephony calls](#making-telephony-calls) - [Call management using
281 lines (207 loc) • 9.28 kB
text/typescript
import EventEmitter from 'events';
import * as qs from 'querystring';
import isPlainObject from 'is-plain-object';
import Externals from '../core/Externals';
function findHeaderName(name, headers) {
name = name.toLowerCase();
return Object.keys(headers).reduce((res, key) => {
if (res) return res;
if (name === key.toLowerCase()) return key;
return res;
}, null);
}
export interface ApiError extends Error {
originalMessage?: string;
response?: Response;
request?: Request;
}
export interface ClientOptions {
externals: Externals;
defaultRequestInit: CreateRequestOptions;
}
export enum events {
beforeRequest = 'beforeRequest',
requestSuccess = 'requestSuccess',
requestError = 'requestError',
}
export default class Client extends EventEmitter {
public static _contentType = 'Content-Type';
public static _jsonContentType = 'application/json';
public static _multipartContentType = 'multipart/mixed';
public static _urlencodedContentType = 'application/x-www-form-urlencoded';
public static _headerSeparator = ':';
public static _bodySeparator = '\n\n';
public static _boundarySeparator = '--';
public static _unauthorizedStatus = 401;
public static _rateLimitStatus = 429;
public static _allowedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'];
public static _defaultRequestInit: CreateRequestOptions = {
credentials: 'include',
mode: 'cors',
};
public events = events;
private _externals: Externals;
private _defaultRequestInit: CreateRequestOptions = {};
public constructor({externals, defaultRequestInit = {}}: ClientOptions) {
super();
this._defaultRequestInit = defaultRequestInit;
this._externals = externals;
}
public async sendRequest(request: Request): Promise<Response> {
let response;
try {
//TODO Stop request if listeners return false
this.emit(this.events.beforeRequest, request);
response = await this._loadResponse(request);
if (!response.ok) throw new Error('Response has unsuccessful status');
this.emit(this.events.requestSuccess, response, request);
return response;
} catch (e) {
const error = !e.response ? await this.makeError(e, response, request) : e;
this.emit(this.events.requestError, error);
throw error;
}
}
public async _loadResponse(request: Request): Promise<Response> {
return this._externals.fetch.call(null, request); // fixed illegal invocation in Chrome
}
/**
* Wraps the JS Error object with transaction information
*/
public async makeError(e: any, response: Response = null, request: Request = null): Promise<ApiError> {
// Wrap only if regular error
if (!e.response && !e.originalMessage) {
e.response = response;
e.request = request;
e.originalMessage = e.message;
e.message = (response && (await this.error(response, true))) || e.originalMessage;
}
return e;
}
public createRequest(init: CreateRequestOptions = Client._defaultRequestInit): Request {
init = {...this._defaultRequestInit, ...init};
init.headers = init.headers || {};
// Sanity checks
if (!init.url) throw new Error('Url is not defined');
if (!init.method) init.method = 'GET';
init.method = init.method.toUpperCase();
if (init.method && Client._allowedMethods.indexOf(init.method) < 0) {
throw new Error(`Method has wrong value: ${init.method}`);
}
// Defaults
init.credentials = init.credentials || 'include';
init.mode = init.mode || 'cors';
// Append Query String
if (init.query) {
init.url = init.url + (init.url.includes('?') ? '&' : '?') + qs.stringify(init.query);
}
if (!findHeaderName('Accept', init.headers)) {
init.headers.Accept = Client._jsonContentType;
}
// Serialize body
if (isPlainObject(init.body) || !init.body) {
let contentTypeHeaderName = findHeaderName(Client._contentType, init.headers);
if (!contentTypeHeaderName) {
contentTypeHeaderName = Client._contentType;
init.headers[contentTypeHeaderName] = Client._jsonContentType;
}
const contentType = init.headers[contentTypeHeaderName];
// Assign a new encoded body
if (contentType.includes(Client._jsonContentType)) {
if ((init.method === 'GET' || init.method === 'HEAD') && !!init.body) {
// oddly setting body to null still result in TypeError in phantomjs
init.body = undefined;
} else {
init.body = JSON.stringify(init.body);
}
} else if (contentType.includes(Client._urlencodedContentType)) {
init.body = qs.stringify(init.body);
}
}
// Create a request with encoded body
const req = new this._externals.Request(init.url, init);
// Keep the original body accessible directly (for mocks)
req.originalBody = init.body;
return req;
}
public _isContentType(contentType, response) {
return this.getContentType(response).includes(contentType);
}
public getContentType(response) {
return response.headers.get(Client._contentType) || '';
}
public isMultipart(response) {
return this._isContentType(Client._multipartContentType, response);
}
public isJson(response) {
return this._isContentType(Client._jsonContentType, response);
}
public async toMultipart(response: Response): Promise<Response[]> {
return this.isMultipart(response) ? this.multipart(response) : [response];
}
public async multipart(response: Response): Promise<Response[]> {
if (!this.isMultipart(response)) throw new Error('Response is not multipart');
// Step 1. Split multipart response
const text = await response.text();
if (!text) throw new Error('No response body');
let boundary;
try {
boundary = this.getContentType(response).match(/boundary=([^;]+)/i)[1]; //eslint-disable-line
} catch (e) {
throw new Error('Cannot find boundary');
}
if (!boundary) throw new Error('Cannot find boundary');
const parts = text.toString().split(Client._boundarySeparator + boundary);
if (parts[0].trim() === '') parts.shift();
if (parts[parts.length - 1].trim() === Client._boundarySeparator) parts.pop();
if (parts.length < 1) throw new Error('No parts in body');
// Step 2. Parse status info
const statusInfo = await this._create(parts.shift(), response.status, response.statusText).json();
// Step 3. Parse all other parts
return parts.map((part, i) => this._create(part, statusInfo.response[i].status));
}
/**
* Method is used to create Response object from string parts of multipart/mixed response
*/
private _create(text = '', status = 200, statusText = 'OK'): Response {
text = text.replace(/\r/g, '');
const headers = new this._externals.Headers();
const headersAndBody = text.split(Client._bodySeparator);
const headersText = headersAndBody.length > 1 ? headersAndBody.shift() : '';
text = headersAndBody.length > 0 ? headersAndBody.join(Client._bodySeparator) : null;
(headersText || '').split('\n').forEach(header => {
const split = header.trim().split(Client._headerSeparator);
const key = split.shift().trim();
const value = split.join(Client._headerSeparator).trim();
if (key) headers.append(key, value);
});
return new this._externals.Response(text, {
headers,
status,
statusText,
});
}
public async error(response: Response, skipOKCheck = false): Promise<string> {
if (response.ok && !skipOKCheck) return null;
let msg = (response.status ? `${response.status} ` : '') + (response.statusText ? response.statusText : '');
try {
const {message, error_description, description} = await response.clone().json();
if (message) msg = message;
if (error_description) msg = error_description;
if (description) msg = description;
} catch (e) {} //eslint-disable-line
return msg;
}
public on(event: events.beforeRequest, listener: (request: Request) => void);
public on(event: events.requestSuccess, listener: (response: Response, request: Request) => void);
public on(event: events.requestError, listener: (error: ApiError) => void);
public on(event: string, listener: (...args) => void) {
return super.on(event, listener);
}
}
export interface CreateRequestOptions extends RequestInit {
url?: string;
body?: any;
query?: any;
headers?: any;
}