Skip to content

Commit

Permalink
refactor: interfaces standardization and ResponseHandlers simplification
Browse files Browse the repository at this point in the history
- Refactor & move interfaces in separate files
- Clear jest cache on npm test command
- Remove UnauthorizedResponseHandler
- Simplify default response handlers
- Test default response handlers
- Export default resposne handlers
- Rename CoveoPlatform for PlatformClient
- Add named export of PlatformClient

BREAKING CHANGE:
- Rename type `IRestResponseHandler` to `ResponseHandler`
- Remove type `IRestError`
- Remove type `IRestResponse`
- All calls return `Promise<T>` directly instead of `Promise<IRestResponse<T>>`
  • Loading branch information
gdostie committed Sep 25, 2019
1 parent 4626628 commit ec0ad12
Show file tree
Hide file tree
Showing 23 changed files with 144 additions and 153 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,18 @@ Each request made by the `platform-client`, once resolved or rejected, gets proc
A response handler is defined as such:

```ts
interface IRestResponseHandler {
interface ResponseHandler {
canProcess(response: Response): boolean; // whether the handler should be used to process the response
process<T>(response: Response): Promise<IRestResponse<T>>; // defines how the handler processes the response
process<T>(response: Response): Promise<T>; // defines how the handler processes the response
}
```

Example

```ts
const MySuccessResponseHandler: IRestResponseHandler = {
const MySuccessResponseHandler: ResponseHandler = {
canProcess: (response: Response): boolean => response.ok;
process: async <T>(response: Response): Promise<IRestResponse<T>> => {
process: async <T>(response: Response): Promise<T> => {
const data = await response.json();
console.log(data);
return data;
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
"version": "0.0.0-development",
"description": "",
"main": "dist/index.js",
"types": "dist/definitions/CoveoPlatform.d.ts",
"types": "dist/definitions/PlatformClient.d.ts",
"scripts": {
"start": "webpack --mode development --watch",
"build": "webpack --mode production",
"test": "jest",
"test": "jest --clearCache && jest",
"test:changed": "jest --watch",
"test:watch": "jest --watchAll",
"release": "semantic-release"
Expand Down
37 changes: 11 additions & 26 deletions src/APICore.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {IRestResponse} from './handlers/HandlerConstants';
import {Handlers, IRestResponseHandler} from './handlers/Handlers';
import {APIConfiguration} from './ConfigurationInterfaces';
import {ResponseHandler} from './handlers/ResponseHandlerInterfaces';
import handleResponse, {defaultResponseHandlers} from './handlers/ResponseHandlers';

function removeEmptyEntries(obj) {
return Object.keys(obj).reduce((memo, key) => {
Expand All @@ -17,57 +18,41 @@ function convertToQueryString(parameters: any) {
return parameters ? `?${new URLSearchParams(Object.entries(removeEmptyEntries(parameters))).toString()}` : '';
}

export interface APIConfiguration {
organizationId: string;
accessTokenRetriever: () => string;
host?: string;
responseHandlers?: IRestResponseHandler[];
}

export default class API {
static orgPlaceholder = '{organizationName}';

constructor(private config: APIConfiguration) {}

async get<T>(url: string, queryParams?: any, args: RequestInit = {method: 'get'}): Promise<IRestResponse<T>> {
async get<T>(url: string, queryParams?: any, args: RequestInit = {method: 'get'}): Promise<T> {
return await this.request<T>(url + convertToQueryString(queryParams), args);
}

async post<T>(
url: string,
body: any,
args: RequestInit = {method: 'post', body: JSON.stringify(body)}
): Promise<IRestResponse<T>> {
): Promise<T> {
return await this.request<T>(url, args);
}

async put<T>(
url: string,
body: any,
args: RequestInit = {method: 'put', body: JSON.stringify(body)}
): Promise<IRestResponse<T>> {
async put<T>(url: string, body: any, args: RequestInit = {method: 'put', body: JSON.stringify(body)}): Promise<T> {
return await this.request<T>(url, args);
}

async delete<T = void>(url: string, args: RequestInit = {method: 'delete'}): Promise<IRestResponse<T>> {
async delete<T = void>(url: string, args: RequestInit = {method: 'delete'}): Promise<T> {
return await this.request<T>(url, args);
}

private handleResponse<T>(response: Response) {
const responseHandler = this.handlers.filter((handler) => handler.canProcess(response))[0];
return responseHandler.process<T>(response);
}

private get handlers(): IRestResponseHandler[] {
private get handlers(): ResponseHandler[] {
const customHandlers = this.config.responseHandlers || [];
return [...customHandlers, ...Handlers];
return customHandlers.length ? customHandlers : defaultResponseHandlers;
}

private getUrlFromRoute(route: string): string {
return `${this.config.host}${route}`.replace(API.orgPlaceholder, this.config.organizationId);
}

private async request<T>(route: string, args: RequestInit): Promise<IRestResponse<T>> {
private async request<T>(route: string, args: RequestInit): Promise<T> {
const init: RequestInit = {
...args,
headers: {
Expand All @@ -78,6 +63,6 @@ export default class API {
};

const response = await fetch(this.getUrlFromRoute(route), init);
return this.handleResponse<T>(response);
return handleResponse<T>(response, this.handlers);
}
}
12 changes: 12 additions & 0 deletions src/ConfigurationInterfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {ResponseHandler} from './handlers/ResponseHandlerInterfaces';

export interface APIConfiguration {
organizationId: string;
accessTokenRetriever: () => string;
host?: string;
responseHandlers?: ResponseHandler[];
}

export interface PlatformClientOptions extends APIConfiguration {
environment?: string;
}
35 changes: 18 additions & 17 deletions src/CoveoPlatform.ts → src/PlatformClient.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
import API, {APIConfiguration} from './APICore';
import {CoveoPlatformResources, Resources} from './resources/Resources';
import API from './APICore';
import {APIConfiguration, PlatformClientOptions} from './ConfigurationInterfaces';
import {ResponseHandlers} from './handlers/ResponseHandlers';
import {PlatformResources, Resources} from './resources/Resources';

export interface CoveoPlatformOptions extends APIConfiguration {
environment?: string;
}

export default class CoveoPlatform extends CoveoPlatformResources {
export class PlatformClient extends PlatformResources {
static Environments = {
dev: 'development',
staging: 'staging',
prod: 'production',
hipaa: 'hipaa',
};
static Hosts = {
[CoveoPlatform.Environments.dev]: 'https://platformdev.cloud.coveo.com',
[CoveoPlatform.Environments.staging]: 'https://platformqa.cloud.coveo.com',
[CoveoPlatform.Environments.prod]: 'https://platform.cloud.coveo.com',
[CoveoPlatform.Environments.hipaa]: 'https://platformhipaa.cloud.coveo.com',
[PlatformClient.Environments.dev]: 'https://platformdev.cloud.coveo.com',
[PlatformClient.Environments.staging]: 'https://platformqa.cloud.coveo.com',
[PlatformClient.Environments.prod]: 'https://platform.cloud.coveo.com',
[PlatformClient.Environments.hipaa]: 'https://platformhipaa.cloud.coveo.com',
};
static defaultOptions: Partial<CoveoPlatformOptions> = {
environment: CoveoPlatform.Environments.prod,
static Handlers = ResponseHandlers;
static defaultOptions: Partial<PlatformClientOptions> = {
environment: PlatformClient.Environments.prod,
responseHandlers: [],
};

private options: CoveoPlatformOptions;
private options: PlatformClientOptions;
private tokenInfo: Record<string, any>; // define a better type
private readonly API: API;

constructor(options: CoveoPlatformOptions) {
constructor(options: PlatformClientOptions) {
super();

this.options = {
...CoveoPlatform.defaultOptions,
...PlatformClient.defaultOptions,
...options,
};

Expand Down Expand Up @@ -60,10 +59,12 @@ export default class CoveoPlatform extends CoveoPlatformResources {
}

private get host(): string {
return this.options.host || CoveoPlatform.Hosts[this.options.environment];
return this.options.host || PlatformClient.Hosts[this.options.environment];
}

private async checkToken() {
return this.API.post('/oauth/check_token', {token: this.options.accessTokenRetriever()});
}
}

export default PlatformClient;
13 changes: 0 additions & 13 deletions src/handlers/ErrorResponseHandler.ts

This file was deleted.

9 changes: 0 additions & 9 deletions src/handlers/HandlerConstants.ts

This file was deleted.

17 changes: 0 additions & 17 deletions src/handlers/Handlers.ts

This file was deleted.

7 changes: 0 additions & 7 deletions src/handlers/NoContentResponseHandler.ts

This file was deleted.

4 changes: 4 additions & 0 deletions src/handlers/ResponseHandlerInterfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ResponseHandler {
canProcess(response: Response): boolean;
process<T>(response: Response): Promise<T>;
}
25 changes: 25 additions & 0 deletions src/handlers/ResponseHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {ResponseHandler} from './ResponseHandlerInterfaces';

const noContent: ResponseHandler = {
canProcess: (response: Response): boolean => response.status === 204,
process: async <T>(): Promise<T> => ({} as T),
};

const success: ResponseHandler = {
canProcess: (response: Response): boolean => response.ok,
process: async <T>(response: Response): Promise<T> => await response.json(),
};

const error: ResponseHandler = {
canProcess: () => true,
process: async <T>(response: Response): Promise<T> => {
throw await response.json();
},
};

export const defaultResponseHandlers = [noContent, success, error];
export const ResponseHandlers = {noContent, success, error};

export default function<T>(response: Response, handlers = defaultResponseHandlers) {
return handlers.filter((handler) => handler.canProcess(response))[0].process<T>(response);
}
7 changes: 0 additions & 7 deletions src/handlers/SuccessResponseHandler.ts

This file was deleted.

17 changes: 0 additions & 17 deletions src/handlers/UnauthorizedResponseHandler.ts

This file was deleted.

34 changes: 34 additions & 0 deletions src/handlers/tests/ResponseHandlers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import handleResponse from '../ResponseHandlers';

describe('ResponseHandlers', () => {
it('should return a promise resolved with an empty object when the response status code is 204', async () => {
const noContentResponse = new Response(null, {status: 204});
const ret = await handleResponse(noContentResponse);
expect(ret).toEqual({});
});

it('should return a promise resolved with the response body when the response status is between 200 and 299', async () => {
const data = {someData: 'thank you!'};

const okResponse = new Response(JSON.stringify(data), {status: 200});
const ret1 = await handleResponse(okResponse);
expect(ret1).toEqual(data);

const stilOkResponse = new Response(JSON.stringify(data), {status: 299});
const ret2 = await handleResponse(stilOkResponse);
expect(ret2).toEqual(data);
});

it('should return a promise rejected with the response body when the status is not between 200 and 299 ', async () => {
const error = {code: 'WRONG_UTENSIL', message: 'Use a spoon to eat the soup.'};
const errorResponse = new Response(JSON.stringify(error), {status: 400});

try {
await handleResponse(errorResponse);
} catch (err) {
expect(err).toEqual(error);
}

expect.assertions(1);
});
});
2 changes: 1 addition & 1 deletion src/resources/Catalogs/Catalog.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import API from '../../APICore';
import {PageModel} from '../BaseInterfaces';
import {CatalogModel, CatalogsListOptions, NewCatalogModel} from './Interfaces';
import {CatalogModel, CatalogsListOptions, NewCatalogModel} from './CatalogInterfaces';

export default class Catalog {
static baseUrl = `/rest/organizations/${API.orgPlaceholder}/catalogs`;
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion src/resources/Catalogs/tests/Catalog.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import API from '../../../APICore';
import Catalog from '../Catalog';
import {CatalogModel, CatalogsListOptions, NewCatalogModel} from '../Interfaces';
import {CatalogModel, CatalogsListOptions, NewCatalogModel} from '../CatalogInterfaces';

jest.mock('../../../APICore');

Expand Down
6 changes: 3 additions & 3 deletions src/resources/Resources.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import API from '../APICore';
import CoveoPlatform from '../CoveoPlatform';
import PlatformClient from '../PlatformClient';
import Catalog from './Catalogs/Catalog';

const resourcesMap = [{key: 'catalog', resource: Catalog}];

export class CoveoPlatformResources {
export class PlatformResources {
catalog: Catalog;
}

const registerAll = (platform: CoveoPlatform, api: API) => {
const registerAll = (platform: PlatformClient, api: API) => {
resourcesMap.forEach(({key, resource}) => {
platform[key] = new resource(api);
});
Expand Down
6 changes: 3 additions & 3 deletions src/resources/tests/Resources.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import API from '../../APICore';
import CoveoPlatform from '../../CoveoPlatform';
import PlatformClient from '../../PlatformClient';
import Catalog from '../Catalogs/Catalog';
import {Resources} from '../Resources';

describe('Resources', () => {
describe('registerAll', () => {
let platform: CoveoPlatform;
let platform: PlatformClient;
let api: API;

beforeEach(() => {
platform = {} as CoveoPlatform;
platform = {} as PlatformClient;
api = {} as API;
});

Expand Down
Loading

0 comments on commit ec0ad12

Please sign in to comment.