UNPKG

graphql-react

Version:

A GraphQL client for React using modern context and hooks APIs that’s lightweight (< 4 kB) but powerful; the first Relay and Apollo alternative with server side rendering.

190 lines (173 loc) 7.14 kB
// @ts-check const ERROR_CODE_FETCH_ERROR = "FETCH_ERROR"; const ERROR_CODE_RESPONSE_HTTP_STATUS = "RESPONSE_HTTP_STATUS"; const ERROR_CODE_RESPONSE_JSON_PARSE_ERROR = "RESPONSE_JSON_PARSE_ERROR"; const ERROR_CODE_RESPONSE_MALFORMED = "RESPONSE_MALFORMED"; /** @typedef {import("./Cache.mjs").CacheValue} CacheValue */ /** @typedef {import("./types.mjs").GraphQLResult} GraphQLResult */ /** * Fetches a GraphQL operation, always resolving a * {@link GraphQLResult GraphQL result} suitable for use as a * {@link CacheValue cache value}, even if there are * {@link FetchGraphQLResultError errors}. * @param {string} fetchUri Fetch URI for the GraphQL API. * @param {RequestInit} [fetchOptions] Fetch options. * @returns {Promise<FetchGraphQLResult>} Resolves a result suitable for use as * a {@link CacheValue cache value}. Shouldn’t reject. * @see [MDN `fetch` parameters docs](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters). * @see [Polyfillable `fetch` options](https://github.github.io/fetch/#options). * Don’t use other options if `fetch` is polyfilled for Node.js or legacy * browsers. */ export default function fetchGraphQL(fetchUri, fetchOptions) { /** @type {FetchGraphQLResult} */ const result = {}; /** @type {Array<FetchGraphQLResultError>} */ const resultErrors = []; const fetcher = typeof fetch === "function" ? fetch : () => Promise.reject(new TypeError("Global `fetch` API unavailable.")); return fetcher(fetchUri, fetchOptions) .then( // Fetch ok. (response) => { // Allow the response to be read in the cache value, but prevent it from // serializing to JSON when sending SSR cache to the client for // hydration. Object.defineProperty(result, "response", { value: response }); if (!response.ok) resultErrors.push( /** @type {import("./types.mjs").GraphQLResultErrorResponseHttpStatus} */ ({ message: `HTTP ${response.status} status.`, extensions: { client: true, code: ERROR_CODE_RESPONSE_HTTP_STATUS, statusCode: response.status, statusText: response.statusText, }, }) ); return response.json().then( // Response JSON parse ok. (json) => { // It’s not safe to assume that the response data format conforms to // the GraphQL spec. // https://spec.graphql.org/October2021/#sec-Response-Format if (typeof json !== "object" || !json || Array.isArray(json)) resultErrors.push( /** @type {import("./types.mjs").GraphQLResultErrorResponseMalformed}*/ ({ message: "Response JSON isn’t an object.", extensions: { client: true, code: ERROR_CODE_RESPONSE_MALFORMED, }, }) ); else { const hasErrors = "errors" in json; const hasData = "data" in json; if (!hasErrors && !hasData) resultErrors.push( /** @type {import("./types.mjs").GraphQLResultErrorResponseMalformed}*/ ({ message: "Response JSON is missing an `errors` or `data` property.", extensions: { client: true, code: ERROR_CODE_RESPONSE_MALFORMED, }, }) ); else { // The `errors` field should be an array, or not set. // https://spec.graphql.org/October2021/#sec-Errors if (hasErrors) if (!Array.isArray(json.errors)) resultErrors.push( /** @type {import("./types.mjs").GraphQLResultErrorResponseMalformed}*/ ({ message: "Response JSON `errors` property isn’t an array.", extensions: { client: true, code: ERROR_CODE_RESPONSE_MALFORMED, }, }) ); else resultErrors.push(...json.errors); // The `data` field should be an object, null, or not set. // https://spec.graphql.org/October2021/#sec-Data if (hasData) if ( // Note that `null` is an object. typeof json.data !== "object" || Array.isArray(json.data) ) resultErrors.push( /** @type {import("./types.mjs").GraphQLResultErrorResponseMalformed}*/ ({ message: "Response JSON `data` property isn’t an object or null.", extensions: { client: true, code: ERROR_CODE_RESPONSE_MALFORMED, }, }) ); else result.data = json.data; } } }, // Response JSON parse error. ({ message }) => { resultErrors.push( /** @type {import("./types.mjs").GraphQLResultErrorResponseJsonParse} */ ({ message: "Response JSON parse error.", extensions: { client: true, code: ERROR_CODE_RESPONSE_JSON_PARSE_ERROR, jsonParseErrorMessage: message, }, }) ); } ); }, // Fetch error. ({ message }) => { resultErrors.push( /** @type {import("./types.mjs").GraphQLResultErrorLoadingFetch} */ ({ message: "Fetch error.", extensions: { client: true, code: ERROR_CODE_FETCH_ERROR, fetchErrorMessage: message, }, }) ); } ) .then(() => { if (resultErrors.length) result.errors = resultErrors; return result; }); } /** * {@linkcode fetchGraphQL} {@link GraphQLResult GraphQL result}. * @typedef {import("./types.mjs").GraphQLResult< * FetchGraphQLResultError * >} FetchGraphQLResult */ /** * {@linkcode fetchGraphQL} {@link GraphQLResult.errors GraphQL result error}. * @typedef {FetchGraphQLResultErrorLoading * | import("./types.mjs").GraphQLResultError * } FetchGraphQLResultError */ /** * {@linkcode fetchGraphQL} {@link GraphQLResult.errors GraphQL result error} * that’s generated on the client, not the GraphQL server. * @typedef {import("./types.mjs").GraphQLResultErrorLoadingFetch * | import("./types.mjs").GraphQLResultErrorResponseHttpStatus * | import("./types.mjs").GraphQLResultErrorResponseJsonParse * | import("./types.mjs").GraphQLResultErrorResponseMalformed * } FetchGraphQLResultErrorLoading */