This repository has been archived by the owner on Feb 6, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #343 from gky360/fix/ts-middleware
Use typescript for middleware
- Loading branch information
Showing
11 changed files
with
258 additions
and
189 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}); | ||
})(); | ||
}; |
Oops, something went wrong.