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
|
/coverage
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/target
|
|
||||||
/public
|
/public
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
|
|
2
examples/movies/.gitignore
vendored
2
examples/movies/.gitignore
vendored
|
@ -21,3 +21,5 @@
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
|
.eslintcache
|
||||||
|
|
3543
examples/movies/package-lock.json
generated
3543
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",
|
"@testing-library/user-event": "^12.2.0",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-dom": "^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-router-dom": "^5.2.0",
|
||||||
"react-scripts": "4.0.0",
|
"react-scripts": "4.0.0",
|
||||||
"web-vitals": "^0.2.4"
|
"web-vitals": "^0.2.4"
|
||||||
|
@ -40,6 +40,6 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react-dom": "^16.9.9",
|
"@types/react-dom": "^16.9.9",
|
||||||
"@types/react-router-dom": "^5.1.6",
|
"@types/react-router-dom": "^5.1.6",
|
||||||
"typescript": "^4.0.5"
|
"typescript": "4.0.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,31 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"rootDir": "src",
|
||||||
|
"outDir": "build",
|
||||||
|
"module": "esnext",
|
||||||
|
"target": "es6",
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
"dom.iterable",
|
"es2020"
|
||||||
"esnext"
|
|
||||||
],
|
],
|
||||||
|
"jsx": "react",
|
||||||
|
"moduleResolution": "node",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
|
"pretty": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react"
|
"strict": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"declaration": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src"
|
"src"
|
||||||
|
|
31
package-lock.json
generated
31
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "react-rest-request",
|
"name": "react-rest-request",
|
||||||
"version": "0.3.2",
|
"version": "0.4.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1067,6 +1067,29 @@
|
||||||
"integrity": "sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw==",
|
"integrity": "sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw==",
|
||||||
"dev": true
|
"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": {
|
"@types/normalize-package-data": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
|
||||||
|
@ -5499,6 +5522,12 @@
|
||||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
|
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
|
||||||
"dev": true
|
"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": {
|
"node-int64": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
|
"build": "rm -rf target && tsc",
|
||||||
"prepublishOnly": "rm -rf target && tsc",
|
"prepublishOnly": "rm -rf target && tsc",
|
||||||
"report-coverage": "cat coverage/lcov.info | coveralls"
|
"report-coverage": "cat coverage/lcov.info | coveralls"
|
||||||
},
|
},
|
||||||
|
@ -40,6 +41,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^26.0.15",
|
"@types/jest": "^26.0.15",
|
||||||
"@types/lodash.isequal": "^4.5.5",
|
"@types/lodash.isequal": "^4.5.5",
|
||||||
|
"@types/node-fetch": "^2.5.7",
|
||||||
"@types/react": "^16.9.55",
|
"@types/react": "^16.9.55",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.6.1",
|
"@typescript-eslint/eslint-plugin": "^4.6.1",
|
||||||
"@typescript-eslint/parser": "^4.6.1",
|
"@typescript-eslint/parser": "^4.6.1",
|
||||||
|
@ -47,6 +49,7 @@
|
||||||
"eslint-plugin-react": "^7.21.5",
|
"eslint-plugin-react": "^7.21.5",
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
|
"node-fetch": "^2.6.1",
|
||||||
"ts-jest": "^26.4.3",
|
"ts-jest": "^26.4.3",
|
||||||
"typescript": "^4.0.5"
|
"typescript": "^4.0.5"
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,12 +25,18 @@ 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: PrepareRequestProps) {
|
public 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 defaultBaseUrl = (window as Window | undefined)?.location.href;
|
||||||
new URL(props.url)
|
const sourceUrl = /https?:\/\//.test(props.url) ?
|
||||||
: new URL(this.config.baseUrl + 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) {
|
if (!requestCanContainBody) {
|
||||||
invariant(!(props.variables instanceof FormData), `Method ${props.method} cannot contain body`);
|
invariant(!(props.variables instanceof FormData), `Method ${props.method} cannot contain body`);
|
||||||
|
|
||||||
|
@ -58,11 +64,14 @@ export class Client {
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
return new Request(url.toString(), {
|
return new Request(
|
||||||
headers,
|
url.toString(),
|
||||||
method: props.method,
|
{
|
||||||
body,
|
headers,
|
||||||
});
|
method: props.method,
|
||||||
|
body,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public request<Data extends Record<string, any>>(
|
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