Skip to content
This repository has been archived by the owner on Feb 6, 2023. It is now read-only.

Commit

Permalink
Merge pull request #343 from gky360/fix/ts-middleware
Browse files Browse the repository at this point in the history
Use typescript for middleware
  • Loading branch information
ofk authored Dec 23, 2019
2 parents 758fc89 + 7acf69c commit 8105a07
Show file tree
Hide file tree
Showing 11 changed files with 258 additions and 189 deletions.
22 changes: 11 additions & 11 deletions frontend/src/actions/entities.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CALL_API } from '../middleware/api';
import { RSAA } from '../middleware/apiMiddleware';
import { fetchResultTypes } from '../constants';

// projects API
Expand All @@ -17,14 +17,14 @@ export const PROJECT_DELETE_SUCCESS = 'PROJECT_DELETE_SUCCESS';
export const PROJECT_DELETE_FAILURE = 'PROJECT_DELETE_FAILURE';

export const getProjectList = () => ({
[CALL_API]: {
[RSAA]: {
types: [PROJECT_LIST_REQUEST, PROJECT_LIST_SUCCESS, PROJECT_LIST_FAILURE],
endpoint: 'projects',
},
});

export const getProject = (projectId) => ({
[CALL_API]: {
[RSAA]: {
types: [PROJECT_REQUEST, PROJECT_SUCCESS, PROJECT_FAILURE],
endpoint: `projects/${projectId}`,
},
Expand All @@ -36,7 +36,7 @@ export const updateProject = (project = {}) => {
throw new Error('Project id is invalid.');
}
return {
[CALL_API]: {
[RSAA]: {
types: [PROJECT_UPDATE_REQUEST, PROJECT_UPDATE_SUCCESS, PROJECT_UPDATE_FAILURE],
endpoint: `projects/${id}`,
method: 'PUT',
Expand All @@ -50,7 +50,7 @@ export const deleteProject = (projectId) => {
throw new Error('Project id is invalid.');
}
return {
[CALL_API]: {
[RSAA]: {
types: [PROJECT_DELETE_REQUEST, PROJECT_DELETE_SUCCESS, PROJECT_DELETE_FAILURE],
endpoint: `projects/${projectId}`,
method: 'DELETE',
Expand Down Expand Up @@ -81,15 +81,15 @@ export const getResultList = (projectId, logsLimit = -1, resultType) => {
const resultTypeQuery = resultType === fetchResultTypes[0].id ? '' : '&is_unregistered=1';

return {
[CALL_API]: {
[RSAA]: {
types: [RESULT_LIST_REQUEST, RESULT_LIST_SUCCESS, RESULT_LIST_FAILURE],
endpoint: `projects/${projectId}/results?logs_limit=${logsLimit}${resultTypeQuery}`,
},
};
};

export const getResult = (projectId, resultId, logsLimit = -1) => ({
[CALL_API]: {
[RSAA]: {
types: [RESULT_REQUEST, RESULT_SUCCESS, RESULT_FAILURE],
endpoint: `projects/${projectId}/results/${resultId}?logs_limit=${logsLimit}`,
},
Expand All @@ -101,7 +101,7 @@ export const updateResult = (projectId, result = {}) => {
throw new Error('Result id is invalid.');
}
return {
[CALL_API]: {
[RSAA]: {
types: [RESULT_UPDATE_REQUEST, RESULT_UPDATE_SUCCESS, RESULT_UPDATE_FAILURE],
endpoint: `projects/${projectId}/results/${id}`,
method: 'PUT',
Expand All @@ -112,7 +112,7 @@ export const updateResult = (projectId, result = {}) => {

export const patchResults = (projectId, results = []) => {
return {
[CALL_API]: {
[RSAA]: {
types: [RESULTS_PATCH_REQUEST, RESULTS_PATCH_SUCCESS, RESULTS_PATCH_FAILURE],
endpoint: `projects/${projectId}/results`,
method: 'PATCH',
Expand All @@ -126,7 +126,7 @@ export const clearResultList = () => ({
});

export const getResultAsset = (projectId, resultId) => ({
[CALL_API]: {
[RSAA]: {
types: [RESULT_ASSET_REQUEST, RESULT_ASSET_SUCCESS, RESULT_ASSET_FAILURE],
endpoint: `projects/${projectId}/results/${resultId}/assets`,
},
Expand All @@ -149,7 +149,7 @@ export const createCommand = (
throw new Error('Result id is invalid.');
}
return {
[CALL_API]: {
[RSAA]: {
types: [COMMAND_CREATE_REQUEST, COMMAND_CREATE_SUCCESS, COMMAND_CREATE_FAILURE],
endpoint: `projects/${projectId}/results/${resultId}/commands`,
method: 'POST',
Expand Down
101 changes: 0 additions & 101 deletions frontend/src/middleware/api.js

This file was deleted.

20 changes: 20 additions & 0 deletions frontend/src/middleware/apiErrors/apiError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export default class ApiError<T = any> extends Error {
name: 'ApiError';

status: number;

statusText: string;

response: T;

message: string;

constructor(status: number, statusText: string, response: T) {
super();
this.name = 'ApiError';
this.status = status;
this.statusText = statusText;
this.response = response;
this.message = `${status} - ${statusText}`;
}
}
11 changes: 11 additions & 0 deletions frontend/src/middleware/apiErrors/requestError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default class RequestError extends Error {
name: 'RequestError';

message: string;

constructor(message: string) {
super();
this.name = 'RequestError';
this.message = message;
}
}
154 changes: 154 additions & 0 deletions frontend/src/middleware/apiMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { Middleware } from 'redux';
import ApiError from './apiErrors/apiError';
import RequestError from './apiErrors/requestError';

export const RSAA = '@@chainerui/RSAA';

export interface RSAACall {
endpoint: string;
method: 'GET' | 'HEAD' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS';
types: [RSAARequestType, RSAASuccessType, RSAAFailureType];
body?: BodyInit | null;
}

export interface RSAAAction {
[RSAA]: RSAACall;
}

type Payload = any;

type Meta = RSAACall & {
httpRequest: {
endpoint: string;
requesting: boolean;
};
};

export interface RSAARequestAction {
type: string;
meta: Meta;
payload?: Payload;
error?: boolean;
}
export interface RSAASuccessAction {
type: string;
meta: Meta;
payload?: Payload;
error?: boolean;
}
export interface RSAAFailureAction {
type: string;
meta: Meta;
payload?: Payload;
error?: boolean;
}

export type RSAARequestType = string;
export type RSAASuccessType = string;
export type RSAAFailureType = string;

export type RSAAActions = RSAARequestAction | RSAASuccessAction | RSAAFailureAction;

const isPlainObject = (obj: any): boolean =>
obj && typeof obj === 'object' && Object.getPrototypeOf(obj) === Object.prototype;

const isRSAAAction = (action: any): action is RSAAAction =>
isPlainObject(action) && Object.prototype.hasOwnProperty.call(action, RSAA);

export const isRSAAActions = (action: any): action is RSAAActions =>
action.meta && action.meta.httpRequest && action.meta.httpRequest.endpoint;

const getJSON = async (res: Response): Promise<any | void> => {
const contentType = res.headers.get('Content-Type');
const emptyCodes = [204, 205];

if (emptyCodes.indexOf(res.status) === -1 && contentType && contentType.indexOf('json') !== -1) {
return res.json();
}
return Promise.resolve();
};

const API_ROOT = '/api/v1/';

const getUrl = (endpoint: string): string =>
endpoint.indexOf(API_ROOT) === -1 ? API_ROOT + endpoint : endpoint;

const normalizeActions = (
types: [RSAARequestType, RSAASuccessType, RSAAFailureType],
callAPI: RSAACall
): [RSAARequestAction, RSAASuccessAction, RSAAFailureAction] => {
const [requestType, successType, failureType] = types;
const { endpoint } = callAPI;

const requestAction = {
type: requestType,
meta: { ...callAPI, httpRequest: { endpoint, requesting: true } },
};
const successAction = {
type: successType,
meta: { ...callAPI, httpRequest: { endpoint, requesting: false } },
};
const failureAction = {
type: failureType,
meta: { ...callAPI, httpRequest: { endpoint, requesting: false } },
error: true,
};
return [requestAction, successAction, failureAction];
};

// TODO: use RootState type
export const apiMiddleware: Middleware = (store) => (next) => (action): any => {
if (!isRSAAAction(action)) {
return next(action);
}

const callAPI = action[RSAA];
const { endpoint, method, types, body } = callAPI;

// Cancel HTTP request if there is already one pending for this URL
const { requests } = store.getState();
if (requests[endpoint]) {
// There is a request for this URL in flight already!
// (Ignore the action)
return undefined;
}

const [requestAction, successAction, failureAction] = normalizeActions(types, callAPI);

next(requestAction);

return (async (): Promise<any> => {
const url = getUrl(endpoint);

let res;
try {
res = await fetch(url, {
method,
body: body ? JSON.stringify(body) : undefined,
headers: {
'Content-Type': 'application/json',
},
});
} catch (e) {
return next({
...failureAction,
payload: new RequestError(e.message),
});
}

const json = await getJSON(res);

const isOk = res.ok;
if (!isOk) {
return next({
...failureAction,
payload: new ApiError(res.status, res.statusText, json),
});
}

return next({
...successAction,
payload: json,
});
})();
};
Loading

0 comments on commit 8105a07

Please sign in to comment.