xref: /expo/packages/@expo/cli/src/api/graphql/client.ts (revision dad749c2)
1import {
2  cacheExchange,
3  Client,
4  CombinedError as GraphqlError,
5  createClient as createUrqlClient,
6  dedupExchange,
7  fetchExchange,
8  OperationContext,
9  OperationResult,
10  PromisifiedSource,
11  TypedDocumentNode,
12} from '@urql/core';
13import { retryExchange } from '@urql/exchange-retry';
14import { DocumentNode } from 'graphql';
15import fetch from 'node-fetch';
16
17import * as Log from '../../log';
18import { getExpoApiBaseUrl } from '../endpoint';
19import UserSettings from '../user/UserSettings';
20
21type AccessTokenHeaders = {
22  authorization: string;
23};
24
25type SessionHeaders = {
26  'expo-session': string;
27};
28
29export const graphqlClient = createUrqlClient({
30  url: getExpoApiBaseUrl() + '/graphql',
31  exchanges: [
32    dedupExchange,
33    cacheExchange,
34    retryExchange({
35      maxDelayMs: 4000,
36      retryIf: (err) =>
37        !!(err && (err.networkError || err.graphQLErrors.some((e) => e?.extensions?.isTransient))),
38    }),
39    fetchExchange,
40  ],
41  // @ts-ignore Type 'typeof fetch' is not assignable to type '(input: RequestInfo, init?: RequestInit | undefined) => Promise<Response>'.
42  fetch,
43  fetchOptions: (): { headers?: AccessTokenHeaders | SessionHeaders } => {
44    const token = UserSettings.getAccessToken();
45    if (token) {
46      return {
47        headers: {
48          authorization: `Bearer ${token}`,
49        },
50      };
51    }
52    const sessionSecret = UserSettings.getSession()?.sessionSecret;
53    if (sessionSecret) {
54      return {
55        headers: {
56          'expo-session': sessionSecret,
57        },
58      };
59    }
60    return {};
61  },
62}) as StricterClient;
63
64/* Please specify additionalTypenames in your Graphql queries */
65export interface StricterClient extends Client {
66  // eslint-disable-next-line @typescript-eslint/ban-types
67  query<Data = any, Variables extends object = {}>(
68    query: DocumentNode | TypedDocumentNode<Data, Variables> | string,
69    variables: Variables | undefined,
70    context: Partial<OperationContext> & { additionalTypenames: string[] }
71  ): PromisifiedSource<OperationResult<Data, Variables>>;
72}
73
74export async function withErrorHandlingAsync<T>(promise: Promise<OperationResult<T>>): Promise<T> {
75  const { data, error } = await promise;
76
77  if (error) {
78    if (error.graphQLErrors.some((e) => e?.extensions?.isTransient)) {
79      Log.error(`We've encountered a transient error, please try again shortly.`);
80    }
81    throw error;
82  }
83
84  // Check for a malformed response. This only checks the root query's existence. It doesn't affect
85  // returning responses with an empty result set.
86  if (!data) {
87    throw new Error('Returned query result data is null!');
88  }
89
90  return data;
91}
92
93export { GraphqlError };
94