├── .gitignore ├── README.md ├── package.json ├── public ├── favicon.ico └── vercel.svg ├── src ├── components │ ├── MakeQuery.js │ ├── Playground.js │ ├── Preview.js │ ├── Result.js │ ├── Toggles.js │ ├── previews │ │ ├── ResolverPreview.js │ │ ├── SchemaPreview.js │ │ └── UseQueryPreview.js │ ├── section.js │ └── toggles │ │ ├── ErrorPolicyToggle.js │ │ ├── NullabilityToggle.js │ │ └── ResolverReturnToggle.js └── pages │ ├── _document.js │ ├── api │ └── graphql.js │ └── index.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚨Apollo Error Handling Visualizer 2 | 3 | 4 | 5 | > This tool shows the relationship between [nullability](http://spec.graphql.org/draft/#sec-Errors-and-Non-Nullability) and error handling in [GraphQL](https://graphql.org/), with specific respect to [Apollo Client](https://www.apollographql.com/docs/react/). 6 | > 7 | > Set the toggles to see how the request / response will change. Scroll to the bottom to see the result! 8 | > 9 | > Questions / Suggestions? Reach out to me at [@mark_larah](https://twitter.com/mark_larah)! 10 | 11 | ![Preview](https://i.fluffy.cc/qnlD8nwnPKVkFKxHLlFSJVXbnb740z6C.png) 12 | 13 | ## Contributing 14 | 15 | - [Clone a fork of the repo](https://guides.github.com/activities/forking/) and install the project dependencies by running `yarn` 16 | - Make your changes, and start the project by running `yarn dev` 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apollo-error-handling", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "@apollo/client": "^3.1.5", 12 | "@graphql-tools/schema": "^6.2.1", 13 | "bumbag": "^1.3.1", 14 | "bumbag-addon-highlighted-code": "^1.3.1", 15 | "bumbag-server": "^1.1.16", 16 | "dedent": "^0.7.0", 17 | "execa": "^4.0.3", 18 | "graphql": "^15.3.0", 19 | "next": "9.5.3", 20 | "react": "16.13.1", 21 | "react-dom": "16.13.1", 22 | "recoil": "^0.0.10" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magicmark/apollo-error-handling-visualizer/625947018027d60effb238fd4a992cb3352f6d96/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/MakeQuery.js: -------------------------------------------------------------------------------- 1 | import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client"; 2 | import { gql, useQuery } from "@apollo/client"; 3 | import { Box, Card, Code, Heading } from "bumbag"; 4 | 5 | import HighlightedCode from "bumbag-addon-highlighted-code"; 6 | import Section from "./section"; 7 | 8 | const GET_BEST_SOCCER_PLAYER = gql` 9 | query { 10 | soccerTeam(name: "Manchester United") { 11 | player(shirtNumber: 9) { 12 | name 13 | } 14 | } 15 | } 16 | `; 17 | 18 | function QueryMaker({ useQueryOptions }) { 19 | const { loading, data, error } = useQuery( 20 | GET_BEST_SOCCER_PLAYER, 21 | useQueryOptions 22 | ); 23 | 24 | return ( 25 | 26 | 33 | Result 34 | 35 | 36 |
40 | Shows the results for data and error{" "} 41 | (from MyComponent - see above). Click the network tab 42 | in developer tools to poke about some more. 43 | 44 | } 45 | > 46 | 47 | 53 | 54 |
55 |
56 |
57 | ); 58 | } 59 | 60 | export default function MakeQuery(props) { 61 | /** 62 | * recreate apollo client on each render to avoid bad cache/sync issues 63 | * @see https://github.com/apollographql/apollo-client/issues/7045 64 | */ 65 | const apolloClient = new ApolloClient({ 66 | cache: new InMemoryCache({ 67 | // hide __typename from the response 68 | addTypename: false, 69 | }), 70 | uri: "/api/graphql", 71 | }); 72 | 73 | return ( 74 | 75 | 76 | 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /src/components/Playground.js: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Card, 4 | Container, 5 | Heading, 6 | Link, 7 | PageContent, 8 | PageWithHeader, 9 | Paragraph, 10 | Stack, 11 | Code, 12 | Text, 13 | TopNav, 14 | applyTheme, 15 | css, 16 | palette, 17 | space, 18 | } from "bumbag"; 19 | 20 | import Preview from "./Preview"; 21 | import Result from "./Result"; 22 | import Toggles from "./Toggles"; 23 | 24 | const BorderBox = applyTheme(Box, { 25 | styles: { 26 | base: (styleProps) => css` 27 | padding: ${space(4, "major")(styleProps)}rem 0; 28 | border-bottom: 1px solid 29 | ${palette("white800", { dark: "gray700" })(styleProps)}; 30 | `, 31 | }, 32 | }); 33 | 34 | export default function Playground({ apolloVersion }) { 35 | return ( 36 | 39 | 40 | 41 | 42 | 43 | 48 | 🚨 49 | 50 | Apollo Error Handling Visualizer 51 | 52 | 53 | 54 | 55 | 56 | 57 | github 61 | 62 | 63 | 64 | 65 | 66 | } 67 | > 68 | 69 | 70 | 71 | 72 | 73 | About 74 | 75 | 76 | 77 | This tool shows the relationship between{" "} 78 | 79 | nullability 80 | {" "} 81 | and error handling in{" "} 82 | GraphQL, with specific 83 | respect to{" "} 84 | 85 | Apollo Client 86 | 87 | . 88 | 89 | 90 | Set the toggles to see how the request / response will change. 91 | Scroll to the bottom to see the result! 92 | 93 | 94 | Questions / Suggestions? Reach out to me at{" "} 95 | @mark_larah! 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | Made by{" "} 111 | 112 | Mark Larah (@mark_larah) 113 | {" "} 114 | • Built with @apollo/client@{apolloVersion} 115 | 116 | 117 | 118 | 119 | ); 120 | } 121 | -------------------------------------------------------------------------------- /src/components/Preview.js: -------------------------------------------------------------------------------- 1 | import { Box, Button, Columns, Heading } from "bumbag"; 2 | 3 | import ResolverPreview from "./previews/ResolverPreview"; 4 | import SchemaPreview from "./previews/SchemaPreview"; 5 | import UseQueryPreview from "./previews/UseQueryPreview"; 6 | import { useState } from "react"; 7 | 8 | export default function Preview() { 9 | const [showPreviews, setShowPreviews] = useState(true); 10 | const togglePreviews = () => setShowPreviews(!showPreviews); 11 | 12 | return ( 13 | 14 |
22 | 29 | Runtime Code 30 | 31 | 39 |
40 | 41 | {showPreviews && ( 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | )} 54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/components/Result.js: -------------------------------------------------------------------------------- 1 | import MakeQuery from "./MakeQuery"; 2 | 3 | import { errorPolicyState } from "./toggles/ErrorPolicyToggle"; 4 | import { nullabilityState } from "./toggles/NullabilityToggle"; 5 | import { resolverBehaviorState } from "./toggles/ResolverReturnToggle"; 6 | import { useRecoilValue } from "recoil"; 7 | 8 | export default function Result() { 9 | const { value: errorPolicy } = useRecoilValue(errorPolicyState); 10 | const { value: nullable } = useRecoilValue(nullabilityState); 11 | const { value: resolverBehaviour } = useRecoilValue(resolverBehaviorState); 12 | 13 | const useQueryOptions = { 14 | errorPolicy, 15 | fetchPolicy: "no-cache", 16 | context: { 17 | headers: { 18 | "X-Demo-Nullable": nullable, 19 | "X-Demo-Resolver-Behaviour": resolverBehaviour, 20 | }, 21 | }, 22 | }; 23 | 24 | return ; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Toggles.js: -------------------------------------------------------------------------------- 1 | import { Box, Columns, Heading, Text } from "bumbag"; 2 | 3 | import ErrorPolicyToggle from "./toggles/ErrorPolicyToggle"; 4 | import NullabilityToggle from "./toggles/NullabilityToggle"; 5 | import ResolverReturnToggle from "./toggles/ResolverReturnToggle"; 6 | 7 | export default function Toggles() { 8 | return ( 9 | 10 | 11 | Toggles 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/previews/ResolverPreview.js: -------------------------------------------------------------------------------- 1 | import { Box, Code, Columns, Heading, Link, Text } from "bumbag"; 2 | 3 | import HighlightedCode from "bumbag-addon-highlighted-code"; 4 | import Section from "../section"; 5 | import dedent from "dedent"; 6 | import { resolverBehaviorState } from "../toggles/ResolverReturnToggle"; 7 | import { useRecoilValue } from "recoil"; 8 | 9 | function getResolver(resolverBehaviour) { 10 | if (!resolverBehaviour) return "Select resolver behaviour!"; 11 | 12 | if (resolverBehaviour === "throwError") { 13 | return dedent` 14 | Player: { 15 | name(obj, args, context, info) { 16 | // Throw a resolver error (that will be caught by graphql-js) 17 | throw new Error('yikes!'); 18 | } 19 | } 20 | `; 21 | } 22 | 23 | if (resolverBehaviour === "networkError") { 24 | return dedent` 25 | Player: { 26 | name(obj, args, context, info) { 27 | // Return HTTP 500 from the server to represent a network level error 28 | throw new InternalServerError(); 29 | } 30 | } 31 | `; 32 | } 33 | 34 | if (resolverBehaviour === "returnValue") { 35 | return dedent` 36 | Player: { 37 | name(obj, args, context, info) { 38 | return 'Anthony Martial'; 39 | } 40 | } 41 | `; 42 | } 43 | 44 | if (resolverBehaviour === "returnNull") { 45 | return dedent` 46 | Player: { 47 | name(obj, args, context, info) { 48 | return null; 49 | } 50 | } 51 | `; 52 | } 53 | } 54 | 55 | export default function SchemaPreview() { 56 | const { value: resolverBehaviour } = useRecoilValue(resolverBehaviorState); 57 | 58 | return ( 59 |
63 | This is the{" "} 64 | 65 | resolver method 66 | {" "} 67 | that will be executed for Player.name.

68 | 69 | } 70 | > 71 | 77 |
78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /src/components/previews/SchemaPreview.js: -------------------------------------------------------------------------------- 1 | import { Box, Code, Columns, Heading, Link, Text } from "bumbag"; 2 | 3 | import HighlightedCode from "bumbag-addon-highlighted-code"; 4 | import Section from "../section"; 5 | import dedent from "dedent"; 6 | import { nullabilityState } from "../toggles/NullabilityToggle"; 7 | import { useRecoilValue } from "recoil"; 8 | 9 | function getSchema(nullability) { 10 | return /* GraphQL*/ dedent` 11 | type Player { 12 | name: ${nullability === "nullable" ? "String" : "String!"} 13 | shirtNumber: Int 14 | position: String 15 | } 16 | 17 | type SoccerTeam { 18 | name: String 19 | player(shirtNumber: Int!): Player 20 | } 21 | 22 | type Query { 23 | soccerTeam(name: String!): SoccerTeam 24 | } 25 | `; 26 | } 27 | 28 | export default function SchemaPreview() { 29 | const { value: nullable } = useRecoilValue(nullabilityState); 30 | 31 | return ( 32 |
36 | This is the{" "} 37 | 38 | Schema Definiton Language (SDL) 39 | {" "} 40 | that the server will use when executing the request. Note the 41 | difference in nullability for Player.name. 42 | 43 | } 44 | > 45 | 51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/components/previews/UseQueryPreview.js: -------------------------------------------------------------------------------- 1 | import { Code, Link, Text } from "bumbag"; 2 | 3 | import HighlightedCode from "bumbag-addon-highlighted-code"; 4 | import Section from "../section"; 5 | import dedent from "dedent"; 6 | import { errorPolicyState } from "../toggles/ErrorPolicyToggle"; 7 | import { useRecoilValue } from "recoil"; 8 | 9 | function getUseQueryCode(errorPolicy) { 10 | if (!errorPolicy) return "Select an error policy!"; 11 | 12 | const getHook = (padding) => { 13 | const padLines = (lines) => 14 | lines.map((l) => `${Array(padding).fill(" ").join("")}${l}`).join("\n"); 15 | 16 | if (errorPolicy === "none") { 17 | return padLines(["const result = useQuery(GET_BEST_PLAYER);"]); 18 | } 19 | 20 | if (errorPolicy === "all") { 21 | return padLines([ 22 | "const result = useQuery(GET_BEST_PLAYER, {", 23 | " errorPolicy: 'all'", 24 | "});", 25 | ]); 26 | } 27 | 28 | if (errorPolicy === "ignore") { 29 | return padLines([ 30 | "const result = useQuery(GET_BEST_PLAYER, {", 31 | " errorPolicy: 'ignore'", 32 | "});", 33 | ]); 34 | } 35 | }; 36 | 37 | return dedent` 38 | const GET_BEST_PLAYER = gql\` 39 | query { 40 | soccerTeam(name: "Manchester United") { 41 | player(shirtNumber: 9) { 42 | name 43 | } 44 | } 45 | } 46 | \`; 47 | 48 | function MyComponent() { 49 | ${getHook(6)} 50 | 51 | const { data, error, loading } = result; 52 | return loading ? null: JSON.stringify({ data, error }); 53 | } 54 | `; 55 | } 56 | 57 | export default function UseQueryPreview() { 58 | const { value: errorPolicy } = useRecoilValue(errorPolicyState); 59 | 60 | return ( 61 |
64 | useQuery React Hook (Client) 65 | 66 | } 67 | description={ 68 | <> 69 | The runtime code the React component will use to make the query. Note 70 | the difference in the errorPolicy attribute. 71 | 72 | } 73 | > 74 | 80 |
81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /src/components/section.js: -------------------------------------------------------------------------------- 1 | import { Box, Heading, Set, Text } from "bumbag"; 2 | 3 | export default function Section({ 4 | heading, 5 | description, 6 | children, 7 | alignBottom, 8 | }) { 9 | return ( 10 | 11 | 12 | 13 | {heading} 14 | 15 | 16 | {description} 17 | 18 | 19 | 20 | 21 | {children} 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/components/toggles/ErrorPolicyToggle.js: -------------------------------------------------------------------------------- 1 | import { Code, Link, SelectMenu, Text } from "bumbag"; 2 | import { atom, useRecoilState } from "recoil"; 3 | 4 | import Section from "../section"; 5 | 6 | const ERROR_POLICY_OPTIONS = [ 7 | { 8 | key: 1, 9 | label: "none", 10 | value: "none", 11 | isDefault: true, 12 | }, 13 | { key: 2, label: "ignore", value: "ignore" }, 14 | { key: 3, label: "all", value: "all" }, 15 | ]; 16 | 17 | export const errorPolicyState = atom({ 18 | key: "errorPolicy", 19 | default: ERROR_POLICY_OPTIONS[0], 20 | }); 21 | 22 | export default function ErrorPolicyToggle() { 23 | const [errorPolicy, setErrorPolicy] = useRecoilState(errorPolicyState); 24 | 25 | return ( 26 |
31 | Toggle errorPolicy to see how the response 32 | objects change.{" "} 33 | 36 | See docs. 37 | 38 | 39 | } 40 | > 41 | ( 44 | 45 | 46 | 47 | 48 |
49 | {isDefault && "(default)"} 50 |
51 | )} 52 | options={ERROR_POLICY_OPTIONS} 53 | placeholder="Select an error policy..." 54 | value={errorPolicy} 55 | /> 56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /src/components/toggles/NullabilityToggle.js: -------------------------------------------------------------------------------- 1 | import {Code, Link, SelectMenu, Text} from "bumbag"; 2 | import { atom, useRecoilState } from "recoil"; 3 | 4 | import Section from "../section"; 5 | 6 | const OPTIONS = [ 7 | { 8 | key: 1, 9 | label: "Nullable (String)", 10 | value: "nullable", 11 | }, 12 | { key: 2, label: "Non-Nullable (String!)", value: "notNullable" }, 13 | ]; 14 | 15 | export const nullabilityState = atom({ 16 | key: "nullability", 17 | default: OPTIONS[0], 18 | }); 19 | 20 | export default function NullabilityToggle() { 21 | const [state, setState] = useRecoilState(nullabilityState); 22 | 23 | return ( 24 |
29 | Toggle if the Player.name field is nullable or not to see 30 | how the response objects change.{" "} 31 | 32 | See docs for errors and nullability. 33 | 34 | 35 | } 36 | > 37 | 43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/components/toggles/ResolverReturnToggle.js: -------------------------------------------------------------------------------- 1 | import { SelectMenu, Text } from "bumbag"; 2 | import { atom, useRecoilState } from "recoil"; 3 | 4 | import Section from "../section"; 5 | 6 | const OPTIONS = [ 7 | 8 | { key: 1, label: "return 'Anthony Martial'", value: "returnValue" }, 9 | { key: 2, label: "return null", value: "returnNull" }, 10 | { 11 | key: 3, 12 | label: "throw new Error('yikes')", 13 | value: "throwError", 14 | }, 15 | { 16 | key: 4, 17 | label: "throw new InternalServerError()", 18 | value: "networkError", 19 | }, 20 | ]; 21 | 22 | export const resolverBehaviorState = atom({ 23 | key: "resolverBehavior", 24 | default: OPTIONS[0], 25 | }); 26 | 27 | export default function ResolverError() { 28 | const [state, setState] = useRecoilState(resolverBehaviorState); 29 | 30 | return ( 31 |
36 | ( 40 | 41 | 42 | 43 | 44 | 45 | )} 46 | options={OPTIONS} 47 | placeholder="Select a resolver error option..." 48 | value={state} 49 | /> 50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/pages/_document.js: -------------------------------------------------------------------------------- 1 | import Document, { Head, Html, Main, NextScript } from "next/document"; 2 | 3 | import { InitializeColorMode } from "bumbag"; 4 | import { extractCritical } from "bumbag-server"; 5 | 6 | export default class MyDocument extends Document { 7 | static async getInitialProps(ctx) { 8 | const initialProps = await Document.getInitialProps(ctx); 9 | const styles = extractCritical(initialProps.html); 10 | return { 11 | ...initialProps, 12 | styles: ( 13 | <> 14 | {initialProps.styles} 15 | 27 | 28 | ); 29 | } 30 | 31 | export async function getStaticProps() { 32 | const { stdout } = await execa.command( 33 | "yarn list --pattern @apollo/client --depth=0 --json" 34 | ); 35 | 36 | const apolloVersion = JSON.parse(stdout).data.trees[0].name.split("@")[2]; 37 | 38 | return { 39 | props: { apolloVersion }, 40 | }; 41 | } 42 | --------------------------------------------------------------------------------