Skip to content
This repository has been archived by the owner on Nov 11, 2023. It is now read-only.

Commit

Permalink
Add support for validators that are promise-based or throw errors
Browse files Browse the repository at this point in the history
  • Loading branch information
peterszerzo committed Oct 30, 2018
1 parent 5bd928c commit 086f94a
Show file tree
Hide file tree
Showing 9 changed files with 152 additions and 26 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ typings/
public
lib

# Cache for parcel serverl in example
.cache

# Files managed by operational-scripts
tslint.json
tsconfig.json
Expand Down
1 change: 1 addition & 0 deletions example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is a simple live demo of `restul-react` components primarily used to test features. Run it with `npm run example`.
2 changes: 2 additions & 0 deletions example/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<div id="app"></div>
<script src="index.tsx"></script>
32 changes: 32 additions & 0 deletions example/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as React from "react";
import { render } from "react-dom";

import { Get, RestfulProvider } from "../src";

const wait = timeout =>
new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, timeout);
});

const App: React.SFC<{}> = props => (
<RestfulProvider base="https://dog.ceo">
<Get path="/api/breeds/list/all" resolve={res => wait(1500).then(() => Promise.resolve(Object.keys(res.message)))}>
{(breeds, { loading, error }) => {
if (loading) {
return "loading..";
}
if (error) {
return <code>{JSON.stringify(error)}</code>;
}
if (breeds) {
return <ul>{breeds.map((breed, breedIndex) => <li key={breedIndex}>{breed}</li>)}</ul>;
}
return null;
}}
</Get>
</RestfulProvider>
);

render(<App />, document.querySelector("#app"));
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
},
"scripts": {
"start": "operational-scripts start",
"example": "parcel example/index.html",
"test": "operational-scripts test",
"build": "operational-scripts build --for npm",
"preversion": "npm run build",
Expand All @@ -54,6 +55,7 @@
"jest-dom": "^1.12.0",
"lint-staged": "^7.2.0",
"nock": "^10.0.0",
"parcel": "^1.10.3",
"prettier": "^1.13.5",
"react-dom": "^16.4.2",
"react-testing-library": "^5.0.0",
Expand All @@ -64,7 +66,7 @@
"typescript": "^3.0.3"
},
"dependencies": {
"lodash": "^4.17.10",
"lodash": "^4.17.11",
"react": "^16.4.1",
"react-fast-compare": "^2.0.1",
"url": "^0.11.0"
Expand Down
85 changes: 62 additions & 23 deletions src/Get.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,68 @@ describe("Get", () => {
await wait(() => expect(children.mock.calls.length).toBe(2));
expect(children.mock.calls[1][0]).toEqual({ hello: "world", foo: "bar" });
});

it("should transform data with a promise", async () => {
nock("https://my-awesome-api.fake")
.get("/")
.reply(200, { hello: "world" });

const children = jest.fn();
children.mockReturnValue(<div />);

render(
<RestfulProvider base="https://my-awesome-api.fake">
<Get path="" resolve={data => Promise.resolve({ ...data, foo: "bar" })}>
{children}
</Get>
</RestfulProvider>,
);

await wait(() => expect(children.mock.calls.length).toBe(2));
expect(children.mock.calls[1][0]).toEqual({ hello: "world", foo: "bar" });
});

it("should pass an error when the resolver throws a runtime error", async () => {
nock("https://my-awesome-api.fake")
.get("/")
.reply(200, { hello: "world" });

const children = jest.fn();
children.mockReturnValue(<div />);

render(
<RestfulProvider base="https://my-awesome-api.fake">
<Get path="" resolve={data => data.apples.oranges}>
{children}
</Get>
</RestfulProvider>,
);

await wait(() => expect(children.mock.calls.length).toBe(2));
expect(children.mock.calls[1][1].error.message).toEqual("RESOLVE_ERROR");
expect(children.mock.calls[1][0]).toEqual(null);
});

it("should pass an error when the resolver is a promise that rejects", async () => {
nock("https://my-awesome-api.fake")
.get("/")
.reply(200, { hello: "world" });

const children = jest.fn();
children.mockReturnValue(<div />);

render(
<RestfulProvider base="https://my-awesome-api.fake">
<Get path="" resolve={data => Promise.reject("nogood")}>
{children}
</Get>
</RestfulProvider>,
);

await wait(() => expect(children.mock.calls.length).toBe(2));
expect(children.mock.calls[1][1].error).toEqual({ message: "RESOLVE_ERROR", data: JSON.stringify("nogood") });
expect(children.mock.calls[1][0]).toEqual(null);
});
});

describe("with lazy", () => {
Expand Down Expand Up @@ -494,23 +556,18 @@ describe("Get", () => {
</RestfulProvider>,
),
);

await wait(() => expect(apiCalls).toEqual(1));
});

it("should call the API only 10 times without debounce", async () => {
let apiCalls = 0;
nock("https://my-awesome-api.fake")
.filteringPath(/test=[^&]*/g, "test=XXX")
.get("/?test=XXX")
.reply(200, () => ++apiCalls)
.persist();

const children = jest.fn();
children.mockReturnValue(<div />);

const resolve = a => a;

/**
* A new instance of RestfulProvider is created on every rerender.
* This will create a new resolve function every time forcing Get to
Expand All @@ -524,15 +581,13 @@ describe("Get", () => {
<Get path="?test=1">{children}</Get>
</RestfulProvider>,
);

times(10, i =>
rerender(
<RestfulProvider base="https://my-awesome-api.fake" resolve={resolve}>
<Get path={`?test=${i + 1}`}>{children}</Get>
</RestfulProvider>,
),
);

expect(apiCalls).toEqual(10);
});
});
Expand All @@ -543,41 +598,34 @@ describe("Get", () => {
.get("/")
.reply(200, () => ++apiCalls)
.persist();

const children = jest.fn();
children.mockReturnValue(<div />);

const resolve = a => a;
const { rerender } = render(
<RestfulProvider base="https://my-awesome-api.fake" resolve={resolve}>
<Get path="">{children}</Get>
</RestfulProvider>,
);

rerender(
<RestfulProvider base="https://my-awesome-api.fake" resolve={resolve}>
<Get path="">{children}</Get>
</RestfulProvider>,
);

expect(apiCalls).toEqual(1);
});
it("should refetch when base changes", () => {
let apiCalls = 0;
nock("https://my-awesome-api.fake")
.get("/")
.reply(200, () => ++apiCalls);

const children = jest.fn();
children.mockReturnValue(<div />);

const resolve = a => a;
const { rerender } = render(
<RestfulProvider base="https://my-awesome-api.fake" resolve={resolve}>
<Get path="">{children}</Get>
</RestfulProvider>,
);

nock("https://my-new-api.fake")
.get("/")
.reply(200, () => ++apiCalls);
Expand All @@ -588,7 +636,6 @@ describe("Get", () => {
</Get>
</RestfulProvider>,
);

expect(apiCalls).toEqual(2);
});
it("should refetch when path changes", () => {
Expand All @@ -598,23 +645,19 @@ describe("Get", () => {
.get("/?test=XXX")
.reply(200, () => ++apiCalls)
.persist();

const children = jest.fn();
children.mockReturnValue(<div />);

const resolve = a => a;
const { rerender } = render(
<RestfulProvider base="https://my-awesome-api.fake" resolve={resolve}>
<Get path="/?test=0">{children}</Get>
</RestfulProvider>,
);

rerender(
<RestfulProvider base="https://my-awesome-api.fake" resolve={resolve}>
<Get path="/?test=1">{children}</Get>
</RestfulProvider>,
);

expect(apiCalls).toEqual(2);
});
it("should refetch when resolve changes", () => {
Expand All @@ -623,17 +666,14 @@ describe("Get", () => {
.get("/")
.reply(200, () => ++apiCalls)
.persist();

const children = jest.fn();
children.mockReturnValue(<div />);

const providerResolve = a => a;
const { rerender } = render(
<RestfulProvider base="https://my-awesome-api.fake" resolve={providerResolve}>
<Get path="">{children}</Get>
</RestfulProvider>,
);

const newResolve = a => a;
rerender(
<RestfulProvider base="https://my-awesome-api.fake" resolve={providerResolve}>
Expand All @@ -642,7 +682,6 @@ describe("Get", () => {
</Get>
</RestfulProvider>,
);

expect(apiCalls).toEqual(2);
});
});
Expand Down
7 changes: 5 additions & 2 deletions src/Get.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import * as React from "react";
import RestfulReactProvider, { InjectedProps, RestfulReactConsumer, RestfulReactProviderProps } from "./Context";
import { composePath, composeUrl } from "./util/composeUrl";
import { processResponse } from "./util/processResponse";
import { resolveData } from "./util/resolveData";

/**
* A function that resolves returned data from
* a fetch call.
*/
export type ResolveFunction<T> = (data: any) => T;
export type ResolveFunction<T> = ((data: any) => T) | ((data: any) => Promise<T>);

export interface GetDataError<TError> {
message: string;
Expand Down Expand Up @@ -237,7 +238,9 @@ class ContextlessGet<TData, TError> extends React.Component<
return null;
}

this.setState({ loading: false, data: resolve!(data) });
const resolved = await resolveData<TData, TError>({ data, resolve });

this.setState({ loading: false, data: resolved.data, error: resolved.error });
return data;
};

Expand Down
12 changes: 12 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Shared types across exported components and utils
//
/**
* A function that resolves returned data from
* a fetch call.
*/
export type ResolveFunction<T> = ((data: any) => T) | ((data: any) => Promise<T>);

export interface GetDataError<TError> {
message: string;
data: TError | string;
}
32 changes: 32 additions & 0 deletions src/util/resolveData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { GetDataError, ResolveFunction } from "../types";

export const resolveData = async <TData, TError>({
data,
resolve,
}: {
data: any;
resolve?: ResolveFunction<TData>;
}): Promise<{ data: TData | null; error: GetDataError<TError> | null }> => {
let resolvedData: TData | null = null;
let resolveError: GetDataError<TError> | null = null;
try {
if (resolve) {
const resolvedDataOrPromise: TData | Promise<TData> = resolve(data);
resolvedData = (resolvedDataOrPromise as { then?: any }).then
? ((await resolvedDataOrPromise) as TData)
: (resolvedDataOrPromise as TData);
} else {
resolvedData = data;
}
} catch (err) {
resolvedData = null;
resolveError = {
message: "RESOLVE_ERROR",
data: JSON.stringify(err),
};
}
return {
data: resolvedData,
error: resolveError,
};
};

0 comments on commit 086f94a

Please sign in to comment.