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

Use typescript for middleware #343

Merged
merged 7 commits into from
Dec 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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