fix: use window location as base url
chore: add dist folder to repo chore(deps): add node-fetch for tests chore: change prepare request visibility chore: add tests for prepare request method chore: update example Closes #20
This commit is contained in:
parent
817b8e46ad
commit
49aac94675
27 changed files with 1899 additions and 2158 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -8,7 +8,6 @@
|
|||
/coverage
|
||||
|
||||
# production
|
||||
/target
|
||||
/public
|
||||
|
||||
# misc
|
||||
|
|
2
examples/movies/.gitignore
vendored
2
examples/movies/.gitignore
vendored
|
@ -21,3 +21,5 @@
|
|||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
.eslintcache
|
||||
|
|
3529
examples/movies/package-lock.json
generated
3529
examples/movies/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -8,7 +8,7 @@
|
|||
"@testing-library/user-event": "^12.2.0",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-rest-request": "file:../../",
|
||||
"react-rest-request": "github:pleshevskiy/react-rest-request",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.0",
|
||||
"web-vitals": "^0.2.4"
|
||||
|
@ -40,6 +40,6 @@
|
|||
"devDependencies": {
|
||||
"@types/react-dom": "^16.9.9",
|
||||
"@types/react-router-dom": "^5.1.6",
|
||||
"typescript": "^4.0.5"
|
||||
"typescript": "4.0.5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,31 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"rootDir": "src",
|
||||
"outDir": "build",
|
||||
"module": "esnext",
|
||||
"target": "es6",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
"es2020"
|
||||
],
|
||||
"jsx": "react",
|
||||
"moduleResolution": "node",
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"pretty": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noImplicitAny": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react"
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
|
|
31
package-lock.json
generated
31
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "react-rest-request",
|
||||
"version": "0.3.2",
|
||||
"version": "0.4.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@ -1067,6 +1067,29 @@
|
|||
"integrity": "sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/node-fetch": {
|
||||
"version": "2.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.7.tgz",
|
||||
"integrity": "sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"form-data": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"form-data": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz",
|
||||
"integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/normalize-package-data": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
|
||||
|
@ -5499,6 +5522,12 @@
|
|||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
||||
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
|
||||
"dev": true
|
||||
},
|
||||
"node-int64": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
],
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"build": "rm -rf target && tsc",
|
||||
"prepublishOnly": "rm -rf target && tsc",
|
||||
"report-coverage": "cat coverage/lcov.info | coveralls"
|
||||
},
|
||||
|
@ -40,6 +41,7 @@
|
|||
"devDependencies": {
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/lodash.isequal": "^4.5.5",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/react": "^16.9.55",
|
||||
"@typescript-eslint/eslint-plugin": "^4.6.1",
|
||||
"@typescript-eslint/parser": "^4.6.1",
|
||||
|
@ -47,6 +49,7 @@
|
|||
"eslint-plugin-react": "^7.21.5",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"jest": "^26.6.3",
|
||||
"node-fetch": "^2.6.1",
|
||||
"ts-jest": "^26.4.3",
|
||||
"typescript": "^4.0.5"
|
||||
}
|
||||
|
|
|
@ -25,12 +25,18 @@ export type ClientResponse<Data extends Record<string, any>> = Readonly<
|
|||
export class Client {
|
||||
constructor(private config: ClientConfig) {}
|
||||
|
||||
private prepareRequest(props: PrepareRequestProps) {
|
||||
public prepareRequest(props: PrepareRequestProps) {
|
||||
const requestCanContainBody = [Method.POST, Method.PATCH, Method.PUT].includes(props.method);
|
||||
|
||||
const url = /https?:\/\//.test(props.url) ?
|
||||
new URL(props.url)
|
||||
: new URL(this.config.baseUrl + props.url);
|
||||
const defaultBaseUrl = (window as Window | undefined)?.location.href;
|
||||
const sourceUrl = /https?:\/\//.test(props.url) ?
|
||||
props.url
|
||||
: this.config.baseUrl + props.url;
|
||||
if (!defaultBaseUrl && sourceUrl.startsWith('/')) {
|
||||
throw new Error(`Invalid request method: ${sourceUrl}`);
|
||||
}
|
||||
|
||||
const url = new URL(sourceUrl, defaultBaseUrl);
|
||||
if (!requestCanContainBody) {
|
||||
invariant(!(props.variables instanceof FormData), `Method ${props.method} cannot contain body`);
|
||||
|
||||
|
@ -58,11 +64,14 @@ export class Client {
|
|||
undefined
|
||||
);
|
||||
|
||||
return new Request(url.toString(), {
|
||||
return new Request(
|
||||
url.toString(),
|
||||
{
|
||||
headers,
|
||||
method: props.method,
|
||||
body,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public request<Data extends Record<string, any>>(
|
||||
|
|
1
target/client-hook.d.ts
vendored
Normal file
1
target/client-hook.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
export declare function useClient(): import("./client").Client[];
|
5
target/client-hook.js
Normal file
5
target/client-hook.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { useRequestContext } from './request-context';
|
||||
export function useClient() {
|
||||
const { client } = useRequestContext();
|
||||
return [client];
|
||||
}
|
23
target/client.d.ts
vendored
Normal file
23
target/client.d.ts
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { Method } from './endpoint';
|
||||
export declare type ClientConfig = {
|
||||
baseUrl: string;
|
||||
};
|
||||
declare type PrepareRequestProps = {
|
||||
url: string;
|
||||
method: Method;
|
||||
headers: Record<string, string>;
|
||||
variables: Record<string, any> | FormData;
|
||||
};
|
||||
export declare type RequestProps<R> = PrepareRequestProps & {
|
||||
transformResponseData?: (data: unknown) => R;
|
||||
};
|
||||
export declare type ClientResponse<Data extends Record<string, any>> = Readonly<Pick<Response, 'ok' | 'redirected' | 'status' | 'statusText' | 'type' | 'headers' | 'url'> & {
|
||||
data: Data;
|
||||
}>;
|
||||
export declare class Client {
|
||||
private config;
|
||||
constructor(config: ClientConfig);
|
||||
private prepareRequest;
|
||||
request<Data extends Record<string, any>>({ transformResponseData, ...restProps }: RequestProps<Data>): Promise<ClientResponse<Data>>;
|
||||
}
|
||||
export {};
|
67
target/client.js
Normal file
67
target/client.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
var __rest = (this && this.__rest) || function (s, e) {
|
||||
var t = {};
|
||||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
||||
t[p] = s[p];
|
||||
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
||||
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
||||
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
||||
t[p[i]] = s[p[i]];
|
||||
}
|
||||
return t;
|
||||
};
|
||||
import invariant from 'tiny-invariant';
|
||||
import { Method } from './endpoint';
|
||||
import { formDataFromObject, isFunction, urlSearchParamsFromObject } from './misc';
|
||||
export class Client {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
}
|
||||
prepareRequest(props) {
|
||||
const requestCanContainBody = [Method.POST, Method.PATCH, Method.PUT].includes(props.method);
|
||||
const url = /https?:\/\//.test(props.url) ?
|
||||
new URL(props.url)
|
||||
: new URL(this.config.baseUrl + props.url);
|
||||
if (!requestCanContainBody) {
|
||||
invariant(!(props.variables instanceof FormData), `Method ${props.method} cannot contain body`);
|
||||
url.search = urlSearchParamsFromObject(props.variables).toString();
|
||||
}
|
||||
const headers = new Headers(props.headers);
|
||||
if (requestCanContainBody && !headers.has('content-type')) {
|
||||
headers.set('content-type', 'application/json');
|
||||
}
|
||||
const contentType = headers.get('content-type');
|
||||
const body = !requestCanContainBody ? (undefined) : contentType === 'application/json' ? (JSON.stringify(props.variables)) : contentType === 'multipart/form-data' ? (props.variables instanceof FormData ? (props.variables) : (formDataFromObject(props.variables))) : (
|
||||
/* TODO: need to add more content-type of body */
|
||||
undefined);
|
||||
return new Request(url.toString(), {
|
||||
headers,
|
||||
method: props.method,
|
||||
body,
|
||||
});
|
||||
}
|
||||
request(_a) {
|
||||
var { transformResponseData } = _a, restProps = __rest(_a, ["transformResponseData"]);
|
||||
const req = this.prepareRequest(restProps);
|
||||
return fetch(req)
|
||||
// TODO: need to check response headers and parse json only if content-type header is application/json
|
||||
.then(res => Promise.all([res, res.json(), false]))
|
||||
.then(([res, data]) => {
|
||||
return {
|
||||
ok: res.ok,
|
||||
redirected: res.redirected,
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
type: res.type,
|
||||
headers: res.headers,
|
||||
url: res.url,
|
||||
data: isFunction(transformResponseData) ? transformResponseData(data) : data,
|
||||
};
|
||||
})
|
||||
.then((res) => {
|
||||
if (!res.ok) {
|
||||
throw res;
|
||||
}
|
||||
return res;
|
||||
});
|
||||
}
|
||||
}
|
14
target/endpoint.d.ts
vendored
Normal file
14
target/endpoint.d.ts
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
export declare enum Method {
|
||||
HEAD = "HEAD",
|
||||
GET = "GET",
|
||||
PUT = "PUT",
|
||||
POST = "POST",
|
||||
PATCH = "PATCH",
|
||||
DELETE = "DELETE"
|
||||
}
|
||||
export declare type Endpoint<R, V, P = void> = Readonly<{
|
||||
method: Method;
|
||||
url: string | ((params: P) => string);
|
||||
headers?: Record<string, string>;
|
||||
transformResponseData?: (data: any) => R;
|
||||
}>;
|
9
target/endpoint.js
Normal file
9
target/endpoint.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
export var Method;
|
||||
(function (Method) {
|
||||
Method["HEAD"] = "HEAD";
|
||||
Method["GET"] = "GET";
|
||||
Method["PUT"] = "PUT";
|
||||
Method["POST"] = "POST";
|
||||
Method["PATCH"] = "PATCH";
|
||||
Method["DELETE"] = "DELETE";
|
||||
})(Method || (Method = {}));
|
7
target/index.d.ts
vendored
Normal file
7
target/index.d.ts
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
export * from './endpoint';
|
||||
export * from './client';
|
||||
export * from './client-hook';
|
||||
export * from './lazy-request-hook';
|
||||
export * from './request-hook';
|
||||
export * from './request-context';
|
||||
export * from './reducer';
|
7
target/index.js
Normal file
7
target/index.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export * from './endpoint';
|
||||
export * from './client';
|
||||
export * from './client-hook';
|
||||
export * from './lazy-request-hook';
|
||||
export * from './request-hook';
|
||||
export * from './request-context';
|
||||
export * from './reducer';
|
15
target/lazy-request-hook.d.ts
vendored
Normal file
15
target/lazy-request-hook.d.ts
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { Endpoint } from './endpoint';
|
||||
import { PublicRequestState } from './reducer';
|
||||
import { ClientResponse } from './client';
|
||||
export declare type LazyRequestConfig<R, V, P = void> = Readonly<{
|
||||
variables?: V;
|
||||
params?: P;
|
||||
headers?: Record<string, string>;
|
||||
onComplete?: (data: R) => unknown;
|
||||
onFailure?: (res: ClientResponse<R>) => unknown;
|
||||
}>;
|
||||
export declare type LazyRequestHandlerConfig<R, V, P> = Readonly<LazyRequestConfig<R, V, P> & {
|
||||
force?: boolean;
|
||||
}>;
|
||||
export declare type RequestHandler<R, V, P> = (config?: LazyRequestHandlerConfig<R, V, P>) => Promise<R | null>;
|
||||
export declare function useLazyRequest<R = Record<string, any>, V = Record<string, any>, P = void>(endpoint: Endpoint<R, V, P>, config?: LazyRequestConfig<R, V, P>): [RequestHandler<R, V, P>, PublicRequestState<R>];
|
75
target/lazy-request-hook.js
Normal file
75
target/lazy-request-hook.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import invariant from 'tiny-invariant';
|
||||
import isEqual from 'lodash.isequal';
|
||||
import { useClient } from './client-hook';
|
||||
import { requestReducer } from './reducer';
|
||||
import { useRequestContext } from './request-context';
|
||||
import { isFunction } from './misc';
|
||||
export function useLazyRequest(endpoint, config) {
|
||||
const [client] = useClient();
|
||||
const { defaultHeaders } = useRequestContext();
|
||||
const [state, dispatch] = React.useReducer(requestReducer, {
|
||||
data: null,
|
||||
loading: false,
|
||||
isCalled: false,
|
||||
});
|
||||
const transformResponseData = useCallback((data) => {
|
||||
return isFunction(endpoint.transformResponseData) ?
|
||||
endpoint.transformResponseData(data)
|
||||
: data;
|
||||
}, [endpoint]);
|
||||
const handler = React.useCallback((handlerConfig) => {
|
||||
var _a, _b, _c;
|
||||
if (state === null || state === void 0 ? void 0 : state.loading) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
let params;
|
||||
let endpointUrl;
|
||||
let isSameRequest = true;
|
||||
if (isFunction(endpoint.url)) {
|
||||
params = (_a = handlerConfig === null || handlerConfig === void 0 ? void 0 : handlerConfig.params) !== null && _a !== void 0 ? _a : config === null || config === void 0 ? void 0 : config.params;
|
||||
invariant(params, 'Endpoind required params');
|
||||
endpointUrl = endpoint.url(params);
|
||||
isSameRequest = !!(state === null || state === void 0 ? void 0 : state.prevParams) && isEqual(state.prevParams, params);
|
||||
}
|
||||
else {
|
||||
endpointUrl = endpoint.url;
|
||||
}
|
||||
const variables = Object.assign(Object.assign({}, config === null || config === void 0 ? void 0 : config.variables), handlerConfig === null || handlerConfig === void 0 ? void 0 : handlerConfig.variables);
|
||||
const headers = Object.assign(Object.assign(Object.assign(Object.assign({}, defaultHeaders), endpoint.headers), config === null || config === void 0 ? void 0 : config.headers), handlerConfig === null || handlerConfig === void 0 ? void 0 : handlerConfig.headers);
|
||||
if (isSameRequest
|
||||
&& (state === null || state === void 0 ? void 0 : state.prevVariables) && isEqual(state.prevVariables, variables)
|
||||
&& (state === null || state === void 0 ? void 0 : state.prevHeaders) && isEqual(state.prevHeaders, headers)
|
||||
&& !(handlerConfig === null || handlerConfig === void 0 ? void 0 : handlerConfig.force)) {
|
||||
return Promise.resolve(state.data);
|
||||
}
|
||||
const onComplete = (_b = handlerConfig === null || handlerConfig === void 0 ? void 0 : handlerConfig.onComplete) !== null && _b !== void 0 ? _b : config === null || config === void 0 ? void 0 : config.onComplete;
|
||||
const onFailure = (_c = handlerConfig === null || handlerConfig === void 0 ? void 0 : handlerConfig.onFailure) !== null && _c !== void 0 ? _c : config === null || config === void 0 ? void 0 : config.onFailure;
|
||||
dispatch({ type: 'call', headers, variables, params });
|
||||
return client
|
||||
.request(Object.assign(Object.assign({}, endpoint), { url: endpointUrl, headers,
|
||||
variables,
|
||||
transformResponseData }))
|
||||
.then((response) => {
|
||||
dispatch({ type: 'success', response });
|
||||
if (isFunction(onComplete)) {
|
||||
onComplete(response.data);
|
||||
}
|
||||
return response.data;
|
||||
}, (response) => {
|
||||
dispatch({ type: 'failure', response });
|
||||
if (isFunction(onFailure)) {
|
||||
onFailure(response);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}, [state, config, client, endpoint, defaultHeaders, transformResponseData]);
|
||||
return [
|
||||
handler,
|
||||
{
|
||||
data: state.data,
|
||||
loading: state.loading,
|
||||
isCalled: state.isCalled,
|
||||
},
|
||||
];
|
||||
}
|
4
target/misc.d.ts
vendored
Normal file
4
target/misc.d.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
export declare function isObject(val: any): val is Record<string, unknown>;
|
||||
export declare function isFunction(val: any): val is (...args: any[]) => any;
|
||||
export declare function formDataFromObject(obj: Record<string, any>): FormData;
|
||||
export declare function urlSearchParamsFromObject(obj: Record<string, any>): URLSearchParams;
|
42
target/misc.js
Normal file
42
target/misc.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
export function isObject(val) {
|
||||
return Object.prototype.toString.call(val) === '[object Object]';
|
||||
}
|
||||
export function isFunction(val) {
|
||||
return typeof val === 'function';
|
||||
}
|
||||
export function formDataFromObject(obj) {
|
||||
const formData = new FormData();
|
||||
Object.entries(obj)
|
||||
.filter(([_, value]) => value !== undefined)
|
||||
.forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
value
|
||||
.filter(val => val !== undefined)
|
||||
.forEach(val => formData.append(key, val));
|
||||
}
|
||||
else if (isObject(value)) {
|
||||
formData.set(key, JSON.stringify(value));
|
||||
}
|
||||
else {
|
||||
formData.set(key, value);
|
||||
}
|
||||
});
|
||||
return formData;
|
||||
}
|
||||
export function urlSearchParamsFromObject(obj) {
|
||||
const searchParams = new URLSearchParams();
|
||||
Object.entries(obj)
|
||||
.filter(([_, value]) => value !== undefined)
|
||||
.forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
const arrayKey = `${key}[]`;
|
||||
value
|
||||
.filter(val => val !== undefined)
|
||||
.forEach(val => searchParams.append(arrayKey, val));
|
||||
}
|
||||
else {
|
||||
searchParams.set(key, value);
|
||||
}
|
||||
});
|
||||
return searchParams;
|
||||
}
|
44
target/reducer.d.ts
vendored
Normal file
44
target/reducer.d.ts
vendored
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { ClientResponse } from './client';
|
||||
export declare type RequestState<R> = Readonly<{
|
||||
data: R | null;
|
||||
loading: boolean;
|
||||
isCalled: boolean;
|
||||
prevHeaders?: Record<string, string>;
|
||||
prevVariables?: Record<string, any>;
|
||||
prevParams?: Record<string, any>;
|
||||
}>;
|
||||
export declare type PublicRequestState<R> = Pick<RequestState<R>, 'data' | 'loading' | 'isCalled'>;
|
||||
export declare type RequestAction<R> = {
|
||||
type: 'call';
|
||||
headers: Record<string, string>;
|
||||
variables: Record<string, any>;
|
||||
params?: Record<string, any>;
|
||||
} | {
|
||||
type: 'success';
|
||||
response: ClientResponse<R>;
|
||||
} | {
|
||||
type: 'failure';
|
||||
response: ClientResponse<R>;
|
||||
};
|
||||
export declare function requestReducer<R>(state: RequestState<R>, action: RequestAction<R>): {
|
||||
loading: boolean;
|
||||
isCalled: boolean;
|
||||
prevHeaders: Record<string, string>;
|
||||
prevVariables: Record<string, any>;
|
||||
prevParams: Record<string, any> | undefined;
|
||||
data: R | null;
|
||||
} | {
|
||||
loading: boolean;
|
||||
data: R;
|
||||
isCalled: boolean;
|
||||
prevHeaders?: Record<string, string> | undefined;
|
||||
prevVariables?: Record<string, any> | undefined;
|
||||
prevParams?: Record<string, any> | undefined;
|
||||
} | {
|
||||
loading: boolean;
|
||||
data: null;
|
||||
isCalled: boolean;
|
||||
prevHeaders?: Record<string, string> | undefined;
|
||||
prevVariables?: Record<string, any> | undefined;
|
||||
prevParams?: Record<string, any> | undefined;
|
||||
};
|
13
target/reducer.js
Normal file
13
target/reducer.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
export function requestReducer(state, action) {
|
||||
switch (action.type) {
|
||||
case 'call': {
|
||||
return Object.assign(Object.assign({}, state), { loading: true, isCalled: true, prevHeaders: action.headers, prevVariables: action.variables, prevParams: action.params });
|
||||
}
|
||||
case 'success': {
|
||||
return Object.assign(Object.assign({}, state), { loading: false, data: action.response.data });
|
||||
}
|
||||
case 'failure': {
|
||||
return Object.assign(Object.assign({}, state), { loading: false, data: null });
|
||||
}
|
||||
}
|
||||
}
|
12
target/request-context.d.ts
vendored
Normal file
12
target/request-context.d.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
import { Client } from './client';
|
||||
export declare type RequestContextData = Readonly<{
|
||||
client: Client;
|
||||
defaultHeaders?: Record<string, string>;
|
||||
}>;
|
||||
export declare type RequestProviderProps = Readonly<React.PropsWithChildren<RequestContextData>>;
|
||||
export declare function RequestProvider({ client, defaultHeaders, children }: RequestProviderProps): JSX.Element;
|
||||
export declare function useRequestContext(): Readonly<{
|
||||
client: Client;
|
||||
defaultHeaders?: Record<string, string> | undefined;
|
||||
}>;
|
11
target/request-context.js
Normal file
11
target/request-context.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
import invariant from 'tiny-invariant';
|
||||
const RequestContext = React.createContext(null);
|
||||
export function RequestProvider({ client, defaultHeaders, children }) {
|
||||
return (React.createElement(RequestContext.Provider, { value: { client, defaultHeaders } }, children));
|
||||
}
|
||||
export function useRequestContext() {
|
||||
const context = React.useContext(RequestContext);
|
||||
invariant(context, 'useRequestContext() must be a child of <RequestProvider />');
|
||||
return context;
|
||||
}
|
13
target/request-hook.d.ts
vendored
Normal file
13
target/request-hook.d.ts
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { Endpoint } from './endpoint';
|
||||
import { LazyRequestConfig } from './lazy-request-hook';
|
||||
export declare type RequestConfig<R, V, P> = Readonly<LazyRequestConfig<R, V, P> & {
|
||||
skip?: boolean;
|
||||
}>;
|
||||
export declare function useRequest<R = Record<string, any>, V = Record<string, any>, P = void>(endpoint: Endpoint<R, V, P>, config?: RequestConfig<R, V, P>): Pick<Readonly<{
|
||||
data: R | null;
|
||||
loading: boolean;
|
||||
isCalled: boolean;
|
||||
prevHeaders?: Record<string, string> | undefined;
|
||||
prevVariables?: Record<string, any> | undefined;
|
||||
prevParams?: Record<string, any> | undefined;
|
||||
}>, "loading" | "data" | "isCalled">;
|
15
target/request-hook.js
Normal file
15
target/request-hook.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
import invariant from 'tiny-invariant';
|
||||
import { Method } from './endpoint';
|
||||
import { useLazyRequest } from './lazy-request-hook';
|
||||
export function useRequest(endpoint, config) {
|
||||
invariant(endpoint.method !== Method.DELETE, `You cannot use useRequest with ${endpoint.method} method`);
|
||||
const [handler, state] = useLazyRequest(endpoint, config);
|
||||
const skip = React.useMemo(() => { var _a; return (_a = config === null || config === void 0 ? void 0 : config.skip) !== null && _a !== void 0 ? _a : false; }, [config]);
|
||||
React.useEffect(() => {
|
||||
if (!skip) {
|
||||
handler();
|
||||
}
|
||||
}, [skip, handler]);
|
||||
return state;
|
||||
}
|
46
tests/client.spec.ts
Normal file
46
tests/client.spec.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { Client } from '../src/client';
|
||||
import { Method } from '../src/endpoint';
|
||||
import * as nodeFetch from 'node-fetch';
|
||||
|
||||
beforeAll(() => {
|
||||
global.Response = nodeFetch.Response as any;
|
||||
global.Request = nodeFetch.Request as any;
|
||||
global.Headers = nodeFetch.Headers as any;
|
||||
});
|
||||
|
||||
|
||||
describe('client', () => {
|
||||
describe('::prepareRequest', () => {
|
||||
it('should prepare request successfully', () => {
|
||||
const client = new Client({
|
||||
baseUrl: 'https://example.org/api'
|
||||
});
|
||||
|
||||
const preparedRequest = client.prepareRequest({
|
||||
url: '/',
|
||||
method: Method.GET,
|
||||
headers: {},
|
||||
variables: {},
|
||||
});
|
||||
|
||||
expect(preparedRequest.url).toBe('https://example.org/api/');
|
||||
expect(preparedRequest.method).toBe(Method.GET);
|
||||
});
|
||||
|
||||
it('should prepare request successfully if client with relative base url', () => {
|
||||
const client = new Client({
|
||||
baseUrl: '/api'
|
||||
});
|
||||
|
||||
const preparedRequest = client.prepareRequest({
|
||||
url: '/',
|
||||
method: Method.GET,
|
||||
headers: {},
|
||||
variables: {},
|
||||
});
|
||||
|
||||
expect(preparedRequest.url).toBe('http://localhost/api/');
|
||||
expect(preparedRequest.method).toBe(Method.GET);
|
||||
});
|
||||
});
|
||||
});
|
Reference in a new issue