From b54bea050351d1363304176f296451cae3c6011f Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Fri, 27 Aug 2021 21:28:03 +0200 Subject: [PATCH] feat(typescript): export GraphqlResponseError (#312) --- README.md | 43 +++++++++++++++++++++++--------------- src/error.ts | 44 ++++++++++++++++++++++++++------------- src/graphql.ts | 9 ++++---- src/index.ts | 1 + test/error.test.ts | 51 ++++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 110 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 7ba46fbd..fe7881cf 100644 --- a/README.md +++ b/README.md @@ -263,10 +263,11 @@ import type { GraphQlQueryResponseData } from "@octokit/graphql"; ## Errors -In case of a GraphQL error, `error.message` is set to the first error from the response’s `errors` array. All errors can be accessed at `error.errors`. `error.request` has the request options such as query, variables and headers set for easier debugging. +In case of a GraphQL error, `error.message` is set to a combined message describing all errors returned by the endpoint. +All errors can be accessed at `error.errors`. `error.request` has the request options such as query, variables and headers set for easier debugging. ```js -let { graphql } = require("@octokit/graphql"); +let { graphql, GraphqlResponseError } = require("@octokit/graphql"); graphqlt = graphql.defaults({ headers: { authorization: `token secret123`, @@ -281,20 +282,30 @@ const query = `{ try { const result = await graphql(query); } catch (error) { - // server responds with - // { - // "data": null, - // "errors": [{ - // "message": "Field 'bioHtml' doesn't exist on type 'User'", - // "locations": [{ - // "line": 3, - // "column": 5 - // }] - // }] - // } - - console.log("Request failed:", error.request); // { query, variables: {}, headers: { authorization: 'token secret123' } } - console.log(error.message); // Field 'bioHtml' doesn't exist on type 'User' + if (error instanceof GraphqlResponseError) { + // do something with the error, allowing you to detect a graphql response error, + // compared to accidentally catching unrelated errors. + + // server responds with an object like the following (as an example) + // class GraphqlResponseError { + // "headers": { + // "status": "403", + // }, + // "data": null, + // "errors": [{ + // "message": "Field 'bioHtml' doesn't exist on type 'User'", + // "locations": [{ + // "line": 3, + // "column": 5 + // }] + // }] + // } + + console.log("Request failed:", error.request); // { query, variables: {}, headers: { authorization: 'token secret123' } } + console.log(error.message); // Field 'bioHtml' doesn't exist on type 'User' + } else { + // handle non-GraphQL error + } } ``` diff --git a/src/error.ts b/src/error.ts index 0338226f..95dbc467 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,22 +1,38 @@ import { ResponseHeaders } from "@octokit/types"; -import { GraphQlEndpointOptions, GraphQlQueryResponse } from "./types"; +import { + GraphQlEndpointOptions, + GraphQlQueryResponse, + GraphQlQueryResponseData, + GraphQlResponse, +} from "./types"; + +type ServerResponseData = Required>; + +function _buildMessageForResponseErrors( + data: ServerResponseData +): string { + return ( + `Request failed due to following response errors:\n` + + data.errors.map((e) => ` - ${e.message}`).join("\n") + ); +} + +export class GraphqlResponseError extends Error { + override name = "GraphqlResponseError"; + + readonly errors: GraphQlQueryResponse["errors"]; + readonly data: ResponseData; -export class GraphqlError extends Error { - public request: GraphQlEndpointOptions; constructor( - request: GraphQlEndpointOptions, - response: { - headers: ResponseHeaders; - data: Required>; - } + readonly request: GraphQlEndpointOptions, + readonly headers: ResponseHeaders, + readonly response: ServerResponseData ) { - const message = response.data.errors[0].message; - super(message); + super(_buildMessageForResponseErrors(response)); - Object.assign(this, response.data); - Object.assign(this, { headers: response.headers }); - this.name = "GraphqlError"; - this.request = request; + // Expose the errors and response data in their shorthand properties. + this.errors = response.errors; + this.data = response.data; // Maintains proper stack trace (only available on V8) /* istanbul ignore next */ diff --git a/src/graphql.ts b/src/graphql.ts index 89a44700..d9861d96 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -1,6 +1,6 @@ import { request as Request } from "@octokit/request"; import { ResponseHeaders } from "@octokit/types"; -import { GraphqlError } from "./error"; +import { GraphqlResponseError } from "./error"; import { GraphQlEndpointOptions, RequestParameters, @@ -76,10 +76,11 @@ export function graphql( headers[key] = response.headers[key]; } - throw new GraphqlError(requestOptions, { + throw new GraphqlResponseError( + requestOptions, headers, - data: response.data as Required>, - }); + response.data as Required> + ); } return response.data.data; diff --git a/src/index.ts b/src/index.ts index cc881d3b..f89546f3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ export const graphql = withDefaults(request, { }); export { GraphQlQueryResponseData } from "./types"; +export { GraphqlResponseError } from "./error"; export function withCustomRequest(customRequest: typeof request) { return withDefaults(customRequest, { diff --git a/test/error.test.ts b/test/error.test.ts index 8b957477..b25b12f2 100644 --- a/test/error.test.ts +++ b/test/error.test.ts @@ -1,6 +1,6 @@ import fetchMock from "fetch-mock"; -import { graphql } from "../src"; +import { graphql, GraphqlResponseError } from "../src"; describe("errors", () => { it("Invalid query", () => { @@ -40,13 +40,55 @@ describe("errors", () => { .catch((error) => { expect(error.message).toEqual( - "Field 'bioHtml' doesn't exist on type 'User'" + "Request failed due to following response errors:\n" + + " - Field 'bioHtml' doesn't exist on type 'User'" ); expect(error.errors).toStrictEqual(mockResponse.errors); expect(error.request.query).toEqual(query); }); }); + it("Should be able check if an error is instance of a GraphQL response error", () => { + const query = `{ + repository { + name + } + }`; + + const mockResponse = { + data: null, + errors: [ + { + locations: [ + { + column: 5, + line: 3, + }, + ], + message: "Some error message", + }, + ], + }; + + return graphql(query, { + headers: { + authorization: `token secret123`, + }, + request: { + fetch: fetchMock + .sandbox() + .post("https://api.github.com/graphql", mockResponse), + }, + }) + .then((result) => { + throw new Error("Should not resolve"); + }) + + .catch((error) => { + expect(error instanceof GraphqlResponseError).toBe(true); + }); + }); + it("Should throw an error for a partial response accompanied by errors", () => { const query = `{ repository(name: "probot", owner: "probot") { @@ -105,7 +147,8 @@ describe("errors", () => { }) .catch((error) => { expect(error.message).toEqual( - "`invalid cursor` does not appear to be a valid cursor." + "Request failed due to following response errors:\n" + + " - `invalid cursor` does not appear to be a valid cursor." ); expect(error.errors).toStrictEqual(mockResponse.errors); expect(error.request.query).toEqual(query); @@ -119,7 +162,7 @@ describe("errors", () => { it("Should throw for server error", () => { const query = `{ - viewer { + viewer { login } }`;