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