From 6d9a648e6dd05d30adb183574e205d45c527a454 Mon Sep 17 00:00:00 2001 From: Tejas Kumar Date: Fri, 3 Aug 2018 18:52:13 +0200 Subject: [PATCH] Fix error handling and resource deletion --- package.json | 2 +- src/Get.tsx | 32 +++++++++++++++++---------- src/Mutate.tsx | 60 +++++++++++++++++++++++++++++++++++--------------- src/Poll.tsx | 7 +++--- yarn.lock | 6 ++--- 5 files changed, 70 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index b342f577..61da06e4 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "jest": "^23.1.0", "lint-staged": "^7.2.0", "prettier": "^1.13.5", - "rollup": "^0.61.2", + "rollup": "^0.63.5", "rollup-plugin-typescript2": "^0.16.1", "ts-jest": "^22.4.6", "tslint": "^5.10.0", diff --git a/src/Get.tsx b/src/Get.tsx index 0564155f..041b022a 100644 --- a/src/Get.tsx +++ b/src/Get.tsx @@ -7,15 +7,20 @@ import RestfulReactProvider, { RestfulReactConsumer, RestfulReactProviderProps } */ export type ResolveFunction = (data: any) => T; +export interface GetDataError { + message: string; + data: S; +} + /** * An enumeration of states that a fetchable * view could possibly have. */ -export interface States { +export interface States { /** Is our view currently loading? */ loading: boolean; /** Do we have an error in the view? */ - error?: string; + error?: GetComponentState["error"]; } /** @@ -41,7 +46,7 @@ export interface Meta { /** * Props for the component. */ -export interface GetComponentProps { +export interface GetComponentProps { /** * The path at which to request data, * typically composed by parent Gets or the RestfulProvider. @@ -54,7 +59,7 @@ export interface GetComponentProps { * @param data - data returned from the request. * @param actions - a key/value map of HTTP verbs, aliasing destroy to DELETE. */ - children: (data: T | null, states: States, actions: Actions, meta: Meta) => React.ReactNode; + children: (data: T | null, states: States, actions: Actions, meta: Meta) => React.ReactNode; /** Options passed into the fetch call. */ requestOptions?: RestfulReactProviderProps["requestOptions"]; /** @@ -85,10 +90,10 @@ export interface GetComponentProps { * are implementation details and should be * hidden from any consumers. */ -export interface GetComponentState { +export interface GetComponentState { data: T | null; response: Response | null; - error: string; + error: GetDataError | null; loading: boolean; } @@ -101,8 +106,8 @@ class ContextlessGet extends React.Component, Readonly> = { data: null, // Means we don't _yet_ have data. response: null, - error: "", loading: !this.props.lazy, + error: null, }; public static defaultProps: Partial> = { @@ -156,19 +161,22 @@ class ContextlessGet extends React.Component, Readonly { const { base, path, resolve } = this.props; - this.setState(() => ({ error: "", loading: true })); + this.setState(() => ({ error: null, loading: true })); const request = new Request(`${base}${requestPath || path || ""}`, this.getRequestOptions(thisRequestOptions)); const response = await fetch(request); + const data: T = + response.headers.get("content-type") === "application/json" ? await response.json() : await response.text(); + if (!response.ok) { - this.setState({ loading: false, error: `Failed to fetch: ${response.status} ${response.statusText}` }); + this.setState({ + loading: false, + error: { message: `Failed to fetch: ${response.status} ${response.statusText}`, data }, + }); throw response; } - const data: T = - response.headers.get("content-type") === "application/json" ? await response.json() : await response.text(); - this.setState({ loading: false, data: resolve!(data) }); return data; }; diff --git a/src/Mutate.tsx b/src/Mutate.tsx index 9233b4ee..5c563653 100644 --- a/src/Mutate.tsx +++ b/src/Mutate.tsx @@ -1,15 +1,16 @@ import * as React from "react"; import RestfulReactProvider, { RestfulReactConsumer, RestfulReactProviderProps } from "./Context"; +import { GetComponentState } from "./Get"; /** * An enumeration of states that a fetchable * view could possibly have. */ -export interface States { +export interface States { /** Is our view currently loading? */ loading: boolean; /** Do we have an error in the view? */ - error?: string; + error?: GetComponentState["error"]; } /** @@ -26,41 +27,58 @@ export interface Meta { /** * Props for the component. */ -export interface MutateComponentProps { +export interface MutateComponentCommonProps { /** * The path at which to request data, * typically composed by parents or the RestfulProvider. */ - path: string; + path?: string; /** * What HTTP verb are we using? */ verb: "POST" | "PUT" | "PATCH" | "DELETE"; + /** + * An escape hatch and an alternative to `path` when you'd like + * to fetch from an entirely different URL. + * + */ + base?: string; + /** Options passed into the fetch call. */ + requestOptions?: RestfulReactProviderProps["requestOptions"]; +} + +export interface MutateComponentWithDelete extends MutateComponentCommonProps { + verb: "DELETE"; /** * A function that recieves a mutation function, along with * some metadata. * * @param actions - a key/value map of HTTP verbs, aliasing destroy to DELETE. */ - children: (mutate: (body?: string | {}) => Promise, states: States, meta: Meta) => React.ReactNode; + children: (mutate: (resourceId?: string | {}) => Promise, states: States, meta: Meta) => React.ReactNode; +} + +export interface MutateComponentWithOtherVerb extends MutateComponentCommonProps { + verb: "POST" | "PUT" | "PATCH"; /** - * An escape hatch and an alternative to `path` when you'd like - * to fetch from an entirely different URL. + * A function that recieves a mutation function, along with + * some metadata. * + * @param actions - a key/value map of HTTP verbs, aliasing destroy to DELETE. */ - base?: string; - /** Options passed into the fetch call. */ - requestOptions?: RestfulReactProviderProps["requestOptions"]; + children: (mutate: (body?: string | {}) => Promise, states: States, meta: Meta) => React.ReactNode; } +export type MutateComponentProps = MutateComponentWithDelete | MutateComponentWithOtherVerb; + /** * State for the component. These * are implementation details and should be * hidden from any consumers. */ -export interface MutateComponentState { +export interface MutateComponentState { response: Response | null; - error: string; + error: GetComponentState["error"]; loading: boolean; } @@ -73,15 +91,16 @@ class ContextlessMutate extends React.Component = { response: null, loading: false, - error: "", + error: null, }; public mutate = async (body?: string | {}, mutateRequestOptions?: RequestInit) => { - const { base, path, verb: method, requestOptions: providerRequestOptions } = this.props; - this.setState(() => ({ error: "", loading: true })); + const { base, path, verb, requestOptions: providerRequestOptions } = this.props; + this.setState(() => ({ error: null, loading: true })); - const request = new Request(`${base}${path || ""}`, { - method, + const requestPath = verb === "DELETE" ? `${base}${path || ""}/${body}` : `${base}${path || ""}`; + const request = new Request(requestPath, { + method: verb, body: typeof body === "object" ? JSON.stringify(body) : body, ...(typeof providerRequestOptions === "function" ? providerRequestOptions() : providerRequestOptions), ...mutateRequestOptions, @@ -93,10 +112,15 @@ class ContextlessMutate extends React.Component { /** * Is there an error? What is it? */ - error?: PollState["error"]; + error: PollState["error"]; } /** @@ -129,7 +129,7 @@ interface PollState { /** * Do we currently have an error? */ - error?: GetComponentState["error"]; + error: GetComponentState["error"]; /** * Index of the last polled response. */ @@ -146,6 +146,7 @@ class ContextlessPoll extends React.Component, Readonly extends React.Component, Readonly