xref: /expo/packages/@expo/cli/src/api/graphql/client.ts (revision 8c8eefe0)
18d307f52SEvan Baconimport {
28d307f52SEvan Bacon  cacheExchange,
38d307f52SEvan Bacon  Client,
48d307f52SEvan Bacon  CombinedError as GraphqlError,
58d307f52SEvan Bacon  createClient as createUrqlClient,
68d307f52SEvan Bacon  dedupExchange,
78d307f52SEvan Bacon  fetchExchange,
88d307f52SEvan Bacon  OperationContext,
98d307f52SEvan Bacon  OperationResult,
108d307f52SEvan Bacon  PromisifiedSource,
118d307f52SEvan Bacon  TypedDocumentNode,
128d307f52SEvan Bacon} from '@urql/core';
138d307f52SEvan Baconimport { retryExchange } from '@urql/exchange-retry';
148d307f52SEvan Baconimport { DocumentNode } from 'graphql';
158d307f52SEvan Baconimport fetch from 'node-fetch';
168d307f52SEvan Bacon
178d307f52SEvan Baconimport * as Log from '../../log';
188d307f52SEvan Baconimport { getExpoApiBaseUrl } from '../endpoint';
19*8c8eefe0SEvan Baconimport { wrapFetchWithOffline } from '../rest/wrapFetchWithOffline';
20*8c8eefe0SEvan Baconimport { wrapFetchWithProxy } from '../rest/wrapFetchWithProxy';
218d307f52SEvan Baconimport UserSettings from '../user/UserSettings';
228d307f52SEvan Bacon
238d307f52SEvan Bacontype AccessTokenHeaders = {
248d307f52SEvan Bacon  authorization: string;
258d307f52SEvan Bacon};
268d307f52SEvan Bacon
278d307f52SEvan Bacontype SessionHeaders = {
288d307f52SEvan Bacon  'expo-session': string;
298d307f52SEvan Bacon};
308d307f52SEvan Bacon
318d307f52SEvan Baconexport const graphqlClient = createUrqlClient({
328d307f52SEvan Bacon  url: getExpoApiBaseUrl() + '/graphql',
338d307f52SEvan Bacon  exchanges: [
348d307f52SEvan Bacon    dedupExchange,
358d307f52SEvan Bacon    cacheExchange,
368d307f52SEvan Bacon    retryExchange({
378d307f52SEvan Bacon      maxDelayMs: 4000,
388d307f52SEvan Bacon      retryIf: (err) =>
398d307f52SEvan Bacon        !!(err && (err.networkError || err.graphQLErrors.some((e) => e?.extensions?.isTransient))),
408d307f52SEvan Bacon    }),
418d307f52SEvan Bacon    fetchExchange,
428d307f52SEvan Bacon  ],
4329975bfdSEvan Bacon  // @ts-ignore Type 'typeof fetch' is not assignable to type '(input: RequestInfo, init?: RequestInit | undefined) => Promise<Response>'.
44*8c8eefe0SEvan Bacon  fetch: wrapFetchWithOffline(wrapFetchWithProxy(fetch)),
458d307f52SEvan Bacon  fetchOptions: (): { headers?: AccessTokenHeaders | SessionHeaders } => {
468d307f52SEvan Bacon    const token = UserSettings.getAccessToken();
478d307f52SEvan Bacon    if (token) {
488d307f52SEvan Bacon      return {
498d307f52SEvan Bacon        headers: {
508d307f52SEvan Bacon          authorization: `Bearer ${token}`,
518d307f52SEvan Bacon        },
528d307f52SEvan Bacon      };
538d307f52SEvan Bacon    }
548d307f52SEvan Bacon    const sessionSecret = UserSettings.getSession()?.sessionSecret;
558d307f52SEvan Bacon    if (sessionSecret) {
568d307f52SEvan Bacon      return {
578d307f52SEvan Bacon        headers: {
588d307f52SEvan Bacon          'expo-session': sessionSecret,
598d307f52SEvan Bacon        },
608d307f52SEvan Bacon      };
618d307f52SEvan Bacon    }
628d307f52SEvan Bacon    return {};
638d307f52SEvan Bacon  },
648d307f52SEvan Bacon}) as StricterClient;
658d307f52SEvan Bacon
668d307f52SEvan Bacon/* Please specify additionalTypenames in your Graphql queries */
678d307f52SEvan Baconexport interface StricterClient extends Client {
688d307f52SEvan Bacon  // eslint-disable-next-line @typescript-eslint/ban-types
698d307f52SEvan Bacon  query<Data = any, Variables extends object = {}>(
708d307f52SEvan Bacon    query: DocumentNode | TypedDocumentNode<Data, Variables> | string,
718d307f52SEvan Bacon    variables: Variables | undefined,
728d307f52SEvan Bacon    context: Partial<OperationContext> & { additionalTypenames: string[] }
738d307f52SEvan Bacon  ): PromisifiedSource<OperationResult<Data, Variables>>;
748d307f52SEvan Bacon}
758d307f52SEvan Bacon
768d307f52SEvan Baconexport async function withErrorHandlingAsync<T>(promise: Promise<OperationResult<T>>): Promise<T> {
778d307f52SEvan Bacon  const { data, error } = await promise;
788d307f52SEvan Bacon
798d307f52SEvan Bacon  if (error) {
808d307f52SEvan Bacon    if (error.graphQLErrors.some((e) => e?.extensions?.isTransient)) {
818d307f52SEvan Bacon      Log.error(`We've encountered a transient error, please try again shortly.`);
828d307f52SEvan Bacon    }
838d307f52SEvan Bacon    throw error;
848d307f52SEvan Bacon  }
858d307f52SEvan Bacon
868d307f52SEvan Bacon  // Check for a malformed response. This only checks the root query's existence. It doesn't affect
878d307f52SEvan Bacon  // returning responses with an empty result set.
888d307f52SEvan Bacon  if (!data) {
898d307f52SEvan Bacon    throw new Error('Returned query result data is null!');
908d307f52SEvan Bacon  }
918d307f52SEvan Bacon
928d307f52SEvan Bacon  return data;
938d307f52SEvan Bacon}
948d307f52SEvan Bacon
958d307f52SEvan Baconexport { GraphqlError };
96