1 | ;
|
2 |
|
3 | const isObject = require('isobject/index.cjs.js');
|
4 |
|
5 | const ERROR_CODE_FETCH_ERROR = 'FETCH_ERROR';
|
6 | const ERROR_CODE_RESPONSE_HTTP_STATUS = 'RESPONSE_HTTP_STATUS';
|
7 | const ERROR_CODE_RESPONSE_JSON_PARSE_ERROR = 'RESPONSE_JSON_PARSE_ERROR';
|
8 | const ERROR_CODE_RESPONSE_MALFORMED = 'RESPONSE_MALFORMED';
|
9 |
|
10 | /**
|
11 | * Fetches a GraphQL operation, always resolving a
|
12 | * [GraphQL result]{@link GraphQLResult} suitable for use as a
|
13 | * [cache value]{@link CacheValue}, even if there are errors. Loading errors
|
14 | * are added to the [GraphQL result]{@link GraphQLResult} `errors` property, and
|
15 | * have an `extensions` property containing `client: true`, along with `code`
|
16 | * and sometimes error-specific properties:
|
17 | *
|
18 | * | Error code | Reasons | Error specific properties |
|
19 | * | :-------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------- |
|
20 | * | `FETCH_ERROR` | Fetch error, e.g. the `fetch` global isn’t defined, or the network is offline. | `fetchErrorMessage` (string). |
|
21 | * | `RESPONSE_HTTP_STATUS` | Response HTTP status code is in the error range. | `statusCode` (number), `statusText` (string). |
|
22 | * | `RESPONSE_JSON_PARSE_ERROR` | Response JSON parse error. | `jsonParseErrorMessage` (string). |
|
23 | * | `RESPONSE_MALFORMED` | Response JSON isn’t an object, is missing an `errors` or `data` property, the `errors` property isn’t an array, or the `data` property isn’t an object or `null`. | |
|
24 | * @kind function
|
25 | * @name fetchGraphQL
|
26 | * @param {string} fetchUri Fetch URI for the GraphQL API.
|
27 | * @param {FetchOptions} fetchOptions Fetch options.
|
28 | * @returns {Promise<GraphQLResult>} Resolves a result suitable for use as a [cache value]{@link CacheValue}. Shouldn’t reject.
|
29 | * @example <caption>Ways to `import`.</caption>
|
30 | * ```js
|
31 | * import { fetchGraphQL } from 'graphql-react';
|
32 | * ```
|
33 | *
|
34 | * ```js
|
35 | * import fetchGraphQL from 'graphql-react/public/fetchGraphQL.js';
|
36 | * ```
|
37 | * @example <caption>Ways to `require`.</caption>
|
38 | * ```js
|
39 | * const { fetchGraphQL } = require('graphql-react');
|
40 | * ```
|
41 | *
|
42 | * ```js
|
43 | * const fetchGraphQL = require('graphql-react/public/fetchGraphQL');
|
44 | * ```
|
45 | */
|
46 | module.exports = function fetchGraphQL(fetchUri, fetchOptions) {
|
47 | const result = { errors: [] };
|
48 | const fetcher =
|
49 | typeof fetch === 'function'
|
50 | ? fetch
|
51 | : () => Promise.reject(new TypeError('Global `fetch` API unavailable.'));
|
52 |
|
53 | return fetcher(fetchUri, fetchOptions)
|
54 | .then(
|
55 | // Fetch ok.
|
56 | (response) => {
|
57 | // Allow the response to be read in the cache value, but prevent it from
|
58 | // serializing to JSON when sending SSR cache to the client for
|
59 | // hydration.
|
60 | Object.defineProperty(result, 'response', { value: response });
|
61 |
|
62 | if (!response.ok)
|
63 | result.errors.push({
|
64 | message: `HTTP ${response.status} status.`,
|
65 | extensions: {
|
66 | client: true,
|
67 | code: ERROR_CODE_RESPONSE_HTTP_STATUS,
|
68 | statusCode: response.status,
|
69 | statusText: response.statusText,
|
70 | },
|
71 | });
|
72 |
|
73 | return response.json().then(
|
74 | // Response JSON parse ok.
|
75 | (json) => {
|
76 | // It’s not safe to assume that the response data format conforms to
|
77 | // the GraphQL spec.
|
78 | // https://spec.graphql.org/June2018/#sec-Response-Format
|
79 |
|
80 | if (!isObject(json))
|
81 | result.errors.push({
|
82 | message: 'Response JSON isn’t an object.',
|
83 | extensions: {
|
84 | client: true,
|
85 | code: ERROR_CODE_RESPONSE_MALFORMED,
|
86 | },
|
87 | });
|
88 | else {
|
89 | const hasErrors = 'errors' in json;
|
90 | const hasData = 'data' in json;
|
91 |
|
92 | if (!hasErrors && !hasData)
|
93 | result.errors.push({
|
94 | message:
|
95 | 'Response JSON is missing an `errors` or `data` property.',
|
96 | extensions: {
|
97 | client: true,
|
98 | code: ERROR_CODE_RESPONSE_MALFORMED,
|
99 | },
|
100 | });
|
101 | else {
|
102 | // The `errors` field should be either an array, or not set.
|
103 | // https://spec.graphql.org/June2018/#sec-Errors
|
104 | if (hasErrors)
|
105 | if (!Array.isArray(json.errors))
|
106 | result.errors.push({
|
107 | message:
|
108 | 'Response JSON `errors` property isn’t an array.',
|
109 | extensions: {
|
110 | client: true,
|
111 | code: ERROR_CODE_RESPONSE_MALFORMED,
|
112 | },
|
113 | });
|
114 | else result.errors.push(...json.errors);
|
115 |
|
116 | // The `data` field should be either an object, null, or not set.
|
117 | // https://spec.graphql.org/June2018/#sec-Data
|
118 | if (hasData)
|
119 | if (!isObject(json.data) && json.data !== null)
|
120 | result.errors.push({
|
121 | message:
|
122 | 'Response JSON `data` property isn’t an object or null.',
|
123 | extensions: {
|
124 | client: true,
|
125 | code: ERROR_CODE_RESPONSE_MALFORMED,
|
126 | },
|
127 | });
|
128 | else result.data = json.data;
|
129 | }
|
130 | }
|
131 | },
|
132 |
|
133 | // Response JSON parse error.
|
134 | ({ message }) => {
|
135 | result.errors.push({
|
136 | message: 'Response JSON parse error.',
|
137 | extensions: {
|
138 | client: true,
|
139 | code: ERROR_CODE_RESPONSE_JSON_PARSE_ERROR,
|
140 | jsonParseErrorMessage: message,
|
141 | },
|
142 | });
|
143 | }
|
144 | );
|
145 | },
|
146 |
|
147 | // Fetch error.
|
148 | ({ message }) => {
|
149 | result.errors.push({
|
150 | message: 'Fetch error.',
|
151 | extensions: {
|
152 | client: true,
|
153 | code: ERROR_CODE_FETCH_ERROR,
|
154 | fetchErrorMessage: message,
|
155 | },
|
156 | });
|
157 | }
|
158 | )
|
159 | .then(() => {
|
160 | if (!result.errors.length) delete result.errors;
|
161 | return result;
|
162 | });
|
163 | };
|