diff --git a/sdk/src/platform/Platform.ts b/sdk/src/platform/Platform.ts index a86f84cd..dfabcf14 100644 --- a/sdk/src/platform/Platform.ts +++ b/sdk/src/platform/Platform.ts @@ -9,6 +9,13 @@ import Cache from '../core/Cache'; import Client, {ApiError} from '../http/Client'; import Externals from '../core/Externals'; import {delay} from './utils'; +import { + parseMiddlewares, + executePreMiddlewaresInSerial, + executePostMiddlewaresInSerial, + executeErrorMiddlewaresInSerial, + Middleware, +} from './middleware'; declare const screen: any; //FIXME TS Crap @@ -77,6 +84,8 @@ export default class Platform extends EventEmitter { private _handleRateLimit: boolean | number; + private _middlewares: Middleware[]; + private _codeVerifier: string; private _discovery?: Discovery; @@ -110,6 +119,7 @@ export default class Platform extends EventEmitter { authProxy = false, urlPrefix = '', handleRateLimit, + middlewares = [], }: PlatformOptionsConstructor) { super(); @@ -140,6 +150,7 @@ export default class Platform extends EventEmitter { this._revokeEndpoint = revokeEndpoint; this._authorizeEndpoint = authorizeEndpoint; this._handleRateLimit = handleRateLimit; + this._middlewares = middlewares; this._codeVerifier = ''; if (enableDiscovery) { const initialEndpoint = discoveryServer @@ -702,20 +713,27 @@ export default class Platform extends EventEmitter { } public async sendRequest(request: Request, options: SendOptions = {}): Promise { + const middlewares = [...this._middlewares, ...(options.middlewares || [])]; + const {preMiddlewares, postMiddlewares, errorMiddlewares} = parseMiddlewares(middlewares); try { request = await this.inflateRequest(request, options); - return await this._client.sendRequest(request); + request = await executePreMiddlewaresInSerial(preMiddlewares, request); + const response = await this._client.sendRequest(request); + return await executePostMiddlewaresInSerial(postMiddlewares, response); } catch (e) { let {retry, handleRateLimit} = options; // Guard is for errors that come from polling - if (!e.response || retry) throw e; + if (!e.response || retry) { + return executeErrorMiddlewaresInSerial(errorMiddlewares, e); + } const {response} = e; const {status} = response; - if ((status !== Client._unauthorizedStatus && status !== Client._rateLimitStatus) || this._authProxy) - throw e; + if ((status !== Client._unauthorizedStatus && status !== Client._rateLimitStatus) || this._authProxy) { + return executeErrorMiddlewaresInSerial(errorMiddlewares, e); + } options.retry = true; @@ -738,7 +756,9 @@ export default class Platform extends EventEmitter { this.emit(this.events.rateLimitError, e); - if (!handleRateLimit) throw e; + if (!handleRateLimit) { + return executeErrorMiddlewaresInSerial(errorMiddlewares, e); + } } await delay(retryAfter); @@ -859,6 +879,7 @@ export interface PlatformOptions extends AuthOptions { discoveryAuthorizedEndpoint?: string; discoveryAutoInit?: boolean; brandId?: string; + middlewares?: Middleware[]; } export interface PlatformOptionsConstructor extends PlatformOptions { @@ -878,6 +899,7 @@ export interface SendOptions { skipDiscoveryCheck?: boolean; handleRateLimit?: boolean | number; retry?: boolean; // Will be set by this method if SDK makes second request + middlewares?: Middleware[]; } export interface LoginOptions { diff --git a/sdk/src/platform/middleware-spec.ts b/sdk/src/platform/middleware-spec.ts new file mode 100644 index 00000000..6f2ec584 --- /dev/null +++ b/sdk/src/platform/middleware-spec.ts @@ -0,0 +1,287 @@ +import {apiCall, asyncTest, expect} from '../test/test'; +import {PreMiddleware, PostMiddleware, ErrorMiddleware} from './middleware'; + +const globalAny: any = global; +const windowAny: any = typeof window !== 'undefined' ? window : global; + +describe('RingCentral.platform.middleware', () => { + describe('add middlewares in sdkOption', () => { + const preMiddleware: PreMiddleware = request => { + request.headers.append('custom-value', 'RC'); + return request; + }; + it( + 'will enhance the request config before send the request', + asyncTest( + async sdk => { + const platform = sdk.platform(); + const customValue = await platform + .send({ + url: 'http://whatever/test/test', + method: 'GET', + }) + .catch(error => { + return error.request.headers.get('custom-value'); + }); + expect(customValue).to.equal('RC'); + }, + { + middlewares: [{pre: preMiddleware}], + }, + ), + ); + }); + + describe('PreMiddleware', () => { + it( + 'will enhance the request config before send the request', + asyncTest(async sdk => { + const preMiddleware: PreMiddleware = request => { + request.headers.append('custom-value', 'RC'); + return request; + }; + const platform = sdk.platform(); + const customValue = await platform + .send({ + url: 'http://whatever/test/test', + method: 'GET', + middlewares: [{pre: preMiddleware}], + }) + .catch(error => { + return error.request.headers.get('custom-value'); + }); + expect(customValue).to.equal('RC'); + }), + ); + it( + 'will enhance the request config twice before send the request', + asyncTest(async sdk => { + const preMiddleware1: PreMiddleware = request => { + request.headers.append('custom-value1', 'RC1'); + return request; + }; + const preMiddleware2: PreMiddleware = request => { + request.headers.append('custom-value2', 'RC2'); + return request; + }; + const platform = sdk.platform(); + const customValue = await platform + .send({ + url: 'http://whatever/test/test', + method: 'GET', + middlewares: [{pre: preMiddleware1}, {pre: preMiddleware2}], + }) + .catch(error => { + return `${error.request.headers.get('custom-value1')} - ${error.request.headers.get( + 'custom-value2', + )}`; + }); + expect(customValue).to.equal('RC1 - RC2'); + }), + ); + }); + + describe('PostMiddleware', () => { + it( + 'will enhance the response after the request was success', + asyncTest(async sdk => { + apiCall('GET', '/test/test', {name: 'RC'}, 200); + const postMiddleware: PostMiddleware = response => { + return response.json() as any; + }; + const platform = sdk.platform(); + const response = await platform.send({ + url: 'http://whatever/test/test', + method: 'GET', + middlewares: [{post: postMiddleware}], + skipAuthCheck: true, + skipDiscoveryCheck: true, + }); + expect(JSON.stringify(response)).to.equal(JSON.stringify({name: 'RC'})); + }), + ); + it( + 'will enhance the response twice after the request was success', + asyncTest(async sdk => { + apiCall('GET', '/test/test', {name: 'RC'}, 200); + const postMiddleware1: PostMiddleware = response => { + return response.json() as any; + }; + const postMiddleware2: PostMiddleware = response => { + return (response as any).name as any; + }; + const platform = sdk.platform(); + const response = await platform.send({ + url: 'http://whatever/test/test', + method: 'GET', + middlewares: [{post: postMiddleware1}, {post: postMiddleware2}], + skipAuthCheck: true, + skipDiscoveryCheck: true, + }); + expect(response).to.equal('RC'); + }), + ); + }); + + describe('ErrorMiddleware', () => { + it( + 'will be treated as success after the request was failed', + asyncTest(async sdk => { + const errorMiddleware: ErrorMiddleware = error => { + error.message = 'Success'; + return error; + }; + const platform = sdk.platform(); + const customValue = await platform + .send({ + url: 'http://whatever/test/test', + method: 'GET', + middlewares: [{error: errorMiddleware}], + }) + .then((res: any) => res.message) + .catch(() => { + return 'Error'; + }); + expect(customValue).to.equal('Success'); + }), + ); + + it( + 'will enhance the error after the request was failed', + asyncTest(async sdk => { + const errorMiddleware: ErrorMiddleware = error => { + error.message = 'Failed'; + throw error; + }; + const platform = sdk.platform(); + const customValue = await platform + .send({ + url: 'http://whatever/test/test', + method: 'GET', + middlewares: [{error: errorMiddleware}], + }) + .then(() => 'Success') + .catch(error => { + return error.message; + }); + expect(customValue).to.equal('Failed'); + }), + ); + + it( + 'will enhance the error twice after the request was failed 1', + asyncTest(async sdk => { + const errorMiddleware1: ErrorMiddleware = error => { + error.message = 'RC'; + throw error; + }; + const errorMiddleware2: ErrorMiddleware = error => { + throw error.message; + }; + const platform = sdk.platform(); + const customValue = await platform + .send({ + url: 'http://whatever/test/test', + method: 'GET', + middlewares: [{error: errorMiddleware1}, {error: errorMiddleware2}], + }) + .catch(error => error); + expect(customValue).to.equal('RC'); + }), + ); + + it( + 'will enhance the error twice after the request was failed 2', + asyncTest(async sdk => { + const errorMiddleware1: ErrorMiddleware = error => { + error.message = 'RC'; + throw error; + }; + const errorMiddleware2: ErrorMiddleware = error => { + return error.message; + }; + const platform = sdk.platform(); + const customValue = await platform + .send({ + url: 'http://whatever/test/test', + method: 'GET', + middlewares: [{error: errorMiddleware1}, {error: errorMiddleware2}], + }) + .catch(() => 'Error'); + expect(customValue).to.equal('RC'); + }), + ); + + it( + 'will enhance the error twice after the request was failed 3', + asyncTest(async sdk => { + const errorMiddleware1: ErrorMiddleware = error => { + error.message = 'RC'; + return error; + }; + const errorMiddleware2: ErrorMiddleware = error => { + throw error.message; + }; + const platform = sdk.platform(); + const customValue: any = await platform + .send({ + url: 'http://whatever/test/test', + method: 'GET', + middlewares: [{error: errorMiddleware1}, {error: errorMiddleware2}], + }) + .catch(() => 'Error'); + expect(customValue.message).to.equal('RC'); + }), + ); + }); + + describe('PostMiddleware & ErrorMiddleware', () => { + it( + 'will goto error middleware if there are some errors in the post middlewares 1', + asyncTest(async sdk => { + apiCall('GET', '/test/test', {name: 'RC'}, 200); + const postMiddleware: PostMiddleware = _response => { + throw new Error('RC'); + }; + const errorMiddleware: ErrorMiddleware = error => { + return error; + }; + const platform = sdk.platform(); + const response = await platform + .send({ + url: 'http://whatever/test/test', + method: 'GET', + middlewares: [{post: postMiddleware, error: errorMiddleware}], + skipAuthCheck: true, + skipDiscoveryCheck: true, + }) + .catch(error => error); + expect(response.message).to.equal('RC'); + }), + ); + + it( + 'will goto error middleware if there are some errors in the post middlewares 2', + asyncTest(async sdk => { + apiCall('GET', '/test/test', {name: 'RC'}, 200); + const postMiddleware: PostMiddleware = _response => { + throw new Error('RC'); + }; + const errorMiddleware: ErrorMiddleware = error => { + throw error.message; + }; + const platform = sdk.platform(); + const response = await platform + .send({ + url: 'http://whatever/test/test', + method: 'GET', + middlewares: [{post: postMiddleware, error: errorMiddleware}], + skipAuthCheck: true, + skipDiscoveryCheck: true, + }) + .catch(error => error); + expect(response).to.equal('RC'); + }), + ); + }); +}); diff --git a/sdk/src/platform/middleware.ts b/sdk/src/platform/middleware.ts new file mode 100644 index 00000000..a5c3d71d --- /dev/null +++ b/sdk/src/platform/middleware.ts @@ -0,0 +1,49 @@ +export interface PreMiddleware { + (request: Request): Promise | Request; +} + +export interface PostMiddleware { + (response: Response | any): Promise | any; +} + +export interface ErrorMiddleware { + (error: Error | any): Promise | any; +} + +export interface Middleware { + pre?: PreMiddleware; + post?: PostMiddleware; + error?: ErrorMiddleware; +} + +function executeMiddlewaresInSerial(middlewares: ((opts: T) => Promise | T)[], opts: T) { + return middlewares.reduce((acc, middleware) => { + return acc.then(middleware); + }, Promise.resolve(opts)); +} + +export function executePreMiddlewaresInSerial(middlewares: PreMiddleware[], request: Request) { + return executeMiddlewaresInSerial(middlewares, request); +} + +export function executePostMiddlewaresInSerial(middlewares: PostMiddleware[], response: Response) { + return executeMiddlewaresInSerial(middlewares, response); +} + +export function executeErrorMiddlewaresInSerial(middlewares: ErrorMiddleware[], error: Error) { + return middlewares.reduce((acc, middleware) => { + return acc.catch(middleware); + }, Promise.reject(error)); +} + +export function parseMiddlewares(middlewares: Middleware[]) { + const preMiddlewares: PreMiddleware[] = []; + const postMiddlewares: PostMiddleware[] = []; + const errorMiddlewares: ErrorMiddleware[] = []; + for (const middleware of middlewares) { + middleware.pre && preMiddlewares.push(middleware.pre); + middleware.post && postMiddlewares.push(middleware.post); + middleware.error && errorMiddlewares.push(middleware.error); + } + return {preMiddlewares, postMiddlewares, errorMiddlewares}; +}