Merge pull request #19 from pleshevskiy/task-3
feat(endpoint): add head method
This commit is contained in:
commit
fd7e7c7585
9 changed files with 106 additions and 24 deletions
33
README.md
33
README.md
|
@ -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[],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
|
@ -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}`,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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;
|
||||||
}>
|
}>
|
||||||
|
|
|
@ -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 [
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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({
|
||||||
|
|
Reference in a new issue