Merge pull request #19 from pleshevskiy/task-3

feat(endpoint): add head method
This commit is contained in:
Dmitriy Pleshevskiy 2020-11-20 19:05:01 +02:00 committed by GitHub
commit fd7e7c7585
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 106 additions and 24 deletions

View file

@ -28,7 +28,7 @@ type Movie = Readonly<{
imdbId: string; imdbId: string;
}> }>
const MoviesEndpoint: Endpoint = { const MoviesEndpoint: Endpoint<MoviesResponse, void> = {
method: Method.GET, method: Method.GET,
url: '/action-adventure', url: '/action-adventure',
}; };
@ -56,3 +56,34 @@ ReactDOM.render(
document.getElementById('root'), document.getElementById('root'),
); );
``` ```
### Transform response
If you have an endpoint that doesn't fit into your beautiful architecture
with its response data, you can transform the response before it's written
to the state.
```typescript
import { Endpoint, Method } from 'react-rest-request';
export type Movie = Readonly<{
id: number;
title: string;
posterURL: string;
imdbId: string;
}>
export const MoviesEndpoint: Endpoint<MoviesResponse, void> = {
method: Method.GET,
url: '/action-adventure',
transformResponseData(data: Movie[]) {
return {
items: data,
}
}
};
export type MoviesResponse = {
items: Movie[],
}
```

View file

@ -7,15 +7,21 @@ export type Movie = Readonly<{
imdbId: string; imdbId: string;
}> }>
export const MoviesEndpoint: Endpoint = { export const MoviesEndpoint: Endpoint<MoviesResponse, void> = {
method: Method.GET, method: Method.GET,
url: '/action-adventure', url: '/action-adventure',
transformResponseData(data: Movie[]) {
return {
items: data,
}
}
}; };
export type MoviesResponse = Movie[]; export type MoviesResponse = {
items: Movie[],
}
export const MovieEndpoint: Endpoint<MovieResponse, void, MovieParams> = {
export const MovieEndpoint: Endpoint<MovieParams> = {
method: Method.GET, method: Method.GET,
url: ({ id }) => `/action-adventure/${id}`, url: ({ id }) => `/action-adventure/${id}`,
}; };

View file

@ -9,9 +9,9 @@ export function MoviesPage() {
return !data ? ( return !data ? (
<div>{ loading ? 'Loading...' : 'Something went wrong' }</div> <div>{ loading ? 'Loading...' : 'Something went wrong' }</div>
) : Array.isArray(data) ? ( ) : (
<ul> <ul>
{data {data.items
.filter(movie => !!movie.title) .filter(movie => !!movie.title)
.map(movie => ( .map(movie => (
<li key={movie.id}> <li key={movie.id}>
@ -22,7 +22,7 @@ export function MoviesPage() {
<Link to='/9999'><span style={{color: 'red'}}>NOT EXIST</span></Link> <Link to='/9999'><span style={{color: 'red'}}>NOT EXIST</span></Link>
</li> </li>
</ul> </ul>
) : <div>{JSON.stringify(data)}</div>; );
} }
export function MoviePage() { export function MoviePage() {

View file

@ -1,18 +1,22 @@
import invariant from 'tiny-invariant'; import invariant from 'tiny-invariant';
import { Method } from './endpoint'; import { Method } from './endpoint';
import { formDataFromObject, urlSearchParamsFromObject } from './misc'; import { formDataFromObject, isFunction, urlSearchParamsFromObject } from './misc';
export type ClientConfig = { export type ClientConfig = {
baseUrl: string, baseUrl: string,
} }
export type RequestProps = { type PrepareRequestProps = {
url: string, url: string,
method: Method, method: Method,
headers: Record<string, string>, headers: Record<string, string>,
variables: Record<string, any> | FormData, variables: Record<string, any> | FormData,
} }
export type RequestProps<R> = PrepareRequestProps & {
transformResponseData?: (data: unknown) => R,
}
export type ClientResponse<Data extends Record<string, any>> = Readonly< export type ClientResponse<Data extends Record<string, any>> = Readonly<
Pick<Response, 'ok' | 'redirected' | 'status' | 'statusText' | 'type' | 'headers' | 'url'> Pick<Response, 'ok' | 'redirected' | 'status' | 'statusText' | 'type' | 'headers' | 'url'>
& { data: Data } & { data: Data }
@ -21,7 +25,7 @@ export type ClientResponse<Data extends Record<string, any>> = Readonly<
export class Client { export class Client {
constructor(private config: ClientConfig) {} constructor(private config: ClientConfig) {}
private prepareRequest(props: RequestProps) { private prepareRequest(props: PrepareRequestProps) {
const requestCanContainBody = [Method.POST, Method.PATCH, Method.PUT].includes(props.method); const requestCanContainBody = [Method.POST, Method.PATCH, Method.PUT].includes(props.method);
const url = /https?:\/\//.test(props.url) ? const url = /https?:\/\//.test(props.url) ?
@ -61,8 +65,13 @@ export class Client {
}); });
} }
public request<Data extends Record<string, any>>(props: RequestProps): Promise<ClientResponse<Data>> { public request<Data extends Record<string, any>>(
const req = this.prepareRequest(props); {
transformResponseData,
...restProps
}: RequestProps<Data>
): Promise<ClientResponse<Data>> {
const req = this.prepareRequest(restProps);
return fetch(req) return fetch(req)
// TODO: need to check response headers and parse json only if content-type header is application/json // TODO: need to check response headers and parse json only if content-type header is application/json
@ -76,7 +85,7 @@ export class Client {
type: res.type, type: res.type,
headers: res.headers, headers: res.headers,
url: res.url, url: res.url,
data data: isFunction(transformResponseData) ? transformResponseData(data) : data,
}; };
}) })
.then((res) => { .then((res) => {

View file

@ -8,8 +8,9 @@ export enum Method {
DELETE = 'DELETE', DELETE = 'DELETE',
} }
export type Endpoint<P = void> = Readonly<{ export type Endpoint<R, V, P = void> = Readonly<{
method: Method; method: Method;
url: string | ((params: P) => string); url: string | ((params: P) => string);
headers?: Record<string, string>; headers?: Record<string, string>;
transformResponseData?: (data: any) => R;
}> }>

View file

@ -1,4 +1,4 @@
import React from 'react'; import React, { useCallback, useMemo } from 'react';
import invariant from 'tiny-invariant'; import invariant from 'tiny-invariant';
import isEqual from 'lodash.isequal'; import isEqual from 'lodash.isequal';
import { useClient } from './client-hook'; import { useClient } from './client-hook';
@ -6,6 +6,7 @@ import { Endpoint } from './endpoint';
import { PublicRequestState, RequestAction, requestReducer, RequestState } from './reducer'; import { PublicRequestState, RequestAction, requestReducer, RequestState } from './reducer';
import { useRequestContext } from './request-context'; import { useRequestContext } from './request-context';
import { ClientResponse } from './client'; import { ClientResponse } from './client';
import { isFunction } from './misc';
export type LazyRequestConfig<R, V, P = void> = Readonly<{ export type LazyRequestConfig<R, V, P = void> = Readonly<{
variables?: V; variables?: V;
@ -23,7 +24,7 @@ export type LazyRequestHandlerConfig<R, V, P> = Readonly<
export type RequestHandler<R, V, P> = (config?: LazyRequestHandlerConfig<R, V, P>) => Promise<R | null>; export type RequestHandler<R, V, P> = (config?: LazyRequestHandlerConfig<R, V, P>) => Promise<R | null>;
export function useLazyRequest<R = Record<string, any>, V = Record<string, any>, P = void>( export function useLazyRequest<R = Record<string, any>, V = Record<string, any>, P = void>(
endpoint: Endpoint<P>, endpoint: Endpoint<R, V, P>,
config?: LazyRequestConfig<R, V, P>, config?: LazyRequestConfig<R, V, P>,
): [RequestHandler<R, V, P>, PublicRequestState<R>] { ): [RequestHandler<R, V, P>, PublicRequestState<R>] {
const [client] = useClient(); const [client] = useClient();
@ -37,6 +38,15 @@ export function useLazyRequest<R = Record<string, any>, V = Record<string, any>,
} }
); );
const transformResponseData = useCallback(
(data: unknown): R => {
return isFunction(endpoint.transformResponseData) ?
endpoint.transformResponseData(data)
: data as R;
},
[endpoint]
);
const handler = React.useCallback( const handler = React.useCallback(
(handlerConfig?: LazyRequestHandlerConfig<R, V, P>) => { (handlerConfig?: LazyRequestHandlerConfig<R, V, P>) => {
if (state?.loading) { if (state?.loading) {
@ -46,7 +56,7 @@ export function useLazyRequest<R = Record<string, any>, V = Record<string, any>,
let params: P | undefined; let params: P | undefined;
let endpointUrl: string; let endpointUrl: string;
let isSameRequest = true; let isSameRequest = true;
if (typeof endpoint.url === 'function') { if (isFunction(endpoint.url)) {
params = handlerConfig?.params ?? config?.params; params = handlerConfig?.params ?? config?.params;
invariant(params, 'Endpoind required params'); invariant(params, 'Endpoind required params');
@ -89,25 +99,28 @@ export function useLazyRequest<R = Record<string, any>, V = Record<string, any>,
url: endpointUrl, url: endpointUrl,
headers, headers,
variables, variables,
transformResponseData,
}) })
.then( .then(
(response) => { (response) => {
dispatch({ type: 'success', response }); dispatch({ type: 'success', response });
if (typeof onComplete === 'function') {
if (isFunction(onComplete)) {
onComplete(response.data); onComplete(response.data);
} }
return response.data; return response.data;
}, },
(response) => { (response) => {
dispatch({ type: 'failure', response }); dispatch({ type: 'failure', response });
if (typeof onFailure === 'function') { if (isFunction(onFailure)) {
onFailure(response); onFailure(response);
} }
return null; return null;
} }
); );
}, },
[state, config, client, endpoint, defaultHeaders] [state, config, client, endpoint, defaultHeaders, transformResponseData]
); );
return [ return [

View file

@ -1,8 +1,12 @@
export function isObject(val: any) { export function isObject(val: any): val is Record<string, unknown> {
return Object.prototype.toString.call(val) === '[object Object]'; return Object.prototype.toString.call(val) === '[object Object]';
} }
export function isFunction(val: any): val is (...args: any[]) => any {
return typeof val === 'function';
}
export function formDataFromObject(obj: Record<string, any>) { export function formDataFromObject(obj: Record<string, any>) {
const formData = new FormData(); const formData = new FormData();
Object.entries(obj) Object.entries(obj)

View file

@ -11,7 +11,7 @@ export type RequestConfig<R, V, P> = Readonly<
> >
export function useRequest<R = Record<string, any>, V = Record<string, any>, P = void>( export function useRequest<R = Record<string, any>, V = Record<string, any>, P = void>(
endpoint: Endpoint<P>, endpoint: Endpoint<R, V, P>,
config?: RequestConfig<R, V, P>, config?: RequestConfig<R, V, P>,
) { ) {
invariant( invariant(

View file

@ -1,4 +1,4 @@
import { formDataFromObject, isObject, urlSearchParamsFromObject } from '../src/misc'; import { formDataFromObject, isFunction, isObject, urlSearchParamsFromObject } from '../src/misc';
describe('misc', () => { describe('misc', () => {
describe('isObject', () => { describe('isObject', () => {
@ -19,6 +19,24 @@ describe('misc', () => {
}); });
}); });
describe('isFunction', () => {
it('should return thruthy successfully', () => {
expect(isFunction(function () { return null; })).toBeTruthy();
});
it('should return falsy', () => {
expect(isFunction(1)).toBeFalsy();
expect(isFunction(true)).toBeFalsy();
expect(isFunction('')).toBeFalsy();
expect(isFunction([])).toBeFalsy();
expect(isFunction({})).toBeFalsy();
expect(isFunction(null)).toBeFalsy();
expect(isFunction(undefined)).toBeFalsy();
expect(isFunction(NaN)).toBeFalsy();
expect(isFunction(Infinity)).toBeFalsy();
});
});
describe('formDataFromObject', () => { describe('formDataFromObject', () => {
it('should convert object to form data successfully', () => { it('should convert object to form data successfully', () => {
const formData = formDataFromObject({ const formData = formDataFromObject({