1 | import { ApolloLink, Observable, RequestHandler, fromError } from 'apollo-link';
|
2 | import {
|
3 | serializeFetchParameter,
|
4 | selectURI,
|
5 | parseAndCheckHttpResponse,
|
6 | checkFetcher,
|
7 | selectHttpOptionsAndBody,
|
8 | createSignalIfSupported,
|
9 | fallbackHttpConfig,
|
10 | Body,
|
11 | HttpOptions,
|
12 | UriFunction as _UriFunction,
|
13 | } from 'apollo-link-http-common';
|
14 | import { DefinitionNode } from 'graphql';
|
15 |
|
16 | export namespace HttpLink {
|
17 |
|
18 | export interface UriFunction extends _UriFunction {}
|
19 | export interface Options extends HttpOptions {
|
20 | |
21 |
|
22 |
|
23 |
|
24 |
|
25 | useGETForQueries?: boolean;
|
26 | }
|
27 | }
|
28 |
|
29 |
|
30 | export import FetchOptions = HttpLink.Options;
|
31 | export import UriFunction = HttpLink.UriFunction;
|
32 |
|
33 | export const createHttpLink = (linkOptions: HttpLink.Options = {}) => {
|
34 | let {
|
35 | uri = '/graphql',
|
36 |
|
37 | fetch: fetcher,
|
38 | includeExtensions,
|
39 | useGETForQueries,
|
40 | ...requestOptions
|
41 | } = linkOptions;
|
42 |
|
43 |
|
44 | checkFetcher(fetcher);
|
45 |
|
46 |
|
47 |
|
48 |
|
49 | if (!fetcher) {
|
50 | fetcher = fetch;
|
51 | }
|
52 |
|
53 | const linkConfig = {
|
54 | http: { includeExtensions },
|
55 | options: requestOptions.fetchOptions,
|
56 | credentials: requestOptions.credentials,
|
57 | headers: requestOptions.headers,
|
58 | };
|
59 |
|
60 | return new ApolloLink(operation => {
|
61 | let chosenURI = selectURI(operation, uri);
|
62 |
|
63 | const context = operation.getContext();
|
64 |
|
65 | const contextConfig = {
|
66 | http: context.http,
|
67 | options: context.fetchOptions,
|
68 | credentials: context.credentials,
|
69 | headers: context.headers,
|
70 | };
|
71 |
|
72 |
|
73 | const { options, body } = selectHttpOptionsAndBody(
|
74 | operation,
|
75 | fallbackHttpConfig,
|
76 | linkConfig,
|
77 | contextConfig,
|
78 | );
|
79 |
|
80 | const { controller, signal } = createSignalIfSupported();
|
81 | if (controller) (options as any).signal = signal;
|
82 |
|
83 |
|
84 | const definitionIsMutation = (d: DefinitionNode) => {
|
85 | return d.kind === 'OperationDefinition' && d.operation === 'mutation';
|
86 | };
|
87 | if (
|
88 | useGETForQueries &&
|
89 | !operation.query.definitions.some(definitionIsMutation)
|
90 | ) {
|
91 | options.method = 'GET';
|
92 | }
|
93 |
|
94 | if (options.method === 'GET') {
|
95 | const { newURI, parseError } = rewriteURIForGET(chosenURI, body);
|
96 | if (parseError) {
|
97 | return fromError(parseError);
|
98 | }
|
99 | chosenURI = newURI;
|
100 | } else {
|
101 | try {
|
102 | (options as any).body = serializeFetchParameter(body, 'Payload');
|
103 | } catch (parseError) {
|
104 | return fromError(parseError);
|
105 | }
|
106 | }
|
107 |
|
108 | return new Observable(observer => {
|
109 | fetcher(chosenURI, options)
|
110 | .then(response => {
|
111 | operation.setContext({ response });
|
112 | return response;
|
113 | })
|
114 | .then(parseAndCheckHttpResponse(operation))
|
115 | .then(result => {
|
116 |
|
117 | observer.next(result);
|
118 | observer.complete();
|
119 | return result;
|
120 | })
|
121 | .catch(err => {
|
122 |
|
123 | if (err.name === 'AbortError') return;
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 | if (err.result && err.result.errors && err.result.data) {
|
131 |
|
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 | observer.next(err.result);
|
158 | }
|
159 | observer.error(err);
|
160 | });
|
161 |
|
162 | return () => {
|
163 |
|
164 |
|
165 | if (controller) controller.abort();
|
166 | };
|
167 | });
|
168 | });
|
169 | };
|
170 |
|
171 |
|
172 |
|
173 | function rewriteURIForGET(chosenURI: string, body: Body) {
|
174 |
|
175 |
|
176 | const queryParams = [];
|
177 | const addQueryParam = (key: string, value: string) => {
|
178 | queryParams.push(`${key}=${encodeURIComponent(value)}`);
|
179 | };
|
180 |
|
181 | if ('query' in body) {
|
182 | addQueryParam('query', body.query);
|
183 | }
|
184 | if (body.operationName) {
|
185 | addQueryParam('operationName', body.operationName);
|
186 | }
|
187 | if (body.variables) {
|
188 | let serializedVariables;
|
189 | try {
|
190 | serializedVariables = serializeFetchParameter(
|
191 | body.variables,
|
192 | 'Variables map',
|
193 | );
|
194 | } catch (parseError) {
|
195 | return { parseError };
|
196 | }
|
197 | addQueryParam('variables', serializedVariables);
|
198 | }
|
199 | if (body.extensions) {
|
200 | let serializedExtensions;
|
201 | try {
|
202 | serializedExtensions = serializeFetchParameter(
|
203 | body.extensions,
|
204 | 'Extensions map',
|
205 | );
|
206 | } catch (parseError) {
|
207 | return { parseError };
|
208 | }
|
209 | addQueryParam('extensions', serializedExtensions);
|
210 | }
|
211 |
|
212 |
|
213 |
|
214 |
|
215 |
|
216 |
|
217 |
|
218 | let fragment = '',
|
219 | preFragment = chosenURI;
|
220 | const fragmentStart = chosenURI.indexOf('#');
|
221 | if (fragmentStart !== -1) {
|
222 | fragment = chosenURI.substr(fragmentStart);
|
223 | preFragment = chosenURI.substr(0, fragmentStart);
|
224 | }
|
225 | const queryParamsPrefix = preFragment.indexOf('?') === -1 ? '?' : '&';
|
226 | const newURI =
|
227 | preFragment + queryParamsPrefix + queryParams.join('&') + fragment;
|
228 | return { newURI };
|
229 | }
|
230 |
|
231 | export class HttpLink extends ApolloLink {
|
232 | public requester: RequestHandler;
|
233 | constructor(opts?: HttpLink.Options) {
|
234 | super(createHttpLink(opts).request);
|
235 | }
|
236 | }
|