Skip to content

Commit 446383b

Browse files
committed
Add optional operationTypeHeader to Apollo client options
When set, HTTP requests carry the GraphQL operation type (query/mutation) as the value of the given header, so a load balancer can route queries to a read replica. Opt-in via new createClient'/createSubscriptionClient'; existing constructors are unchanged.
1 parent 9e2bf80 commit 446383b

2 files changed

Lines changed: 82 additions & 13 deletions

File tree

‎src/GraphQL/Client/BaseClients/Apollo.js‎

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { createClient as createWsClient } from "graphql-ws";
22
import {
33
gql,
44
split,
5-
HttpLink,
65
createHttpLink,
76
InMemoryCache,
87
ApolloClient,
@@ -11,6 +10,37 @@ import { getMainDefinition } from "@apollo/client/utilities/index.js";
1110
import { setContext } from "@apollo/client/link/context/index.js";
1211
import { GraphQLWsLink } from "@apollo/client/link/subscriptions/index.js";
1312

13+
// Build the HTTP link. When opts.operationTypeHeader is set, requests are
14+
// split by operation type and the type ("query" or "mutation") is sent as
15+
// the value of that header, so eg. a load balancer can route queries to a
16+
// read replica.
17+
const mkHttpLink = function (opts) {
18+
if (!opts.operationTypeHeader) {
19+
return createHttpLink({
20+
uri: opts.url,
21+
});
22+
}
23+
24+
const linkForOperationType = function (operationType) {
25+
return createHttpLink({
26+
uri: opts.url,
27+
headers: { [opts.operationTypeHeader]: operationType },
28+
});
29+
};
30+
31+
return split(
32+
function ({ query }) {
33+
const definition = getMainDefinition(query);
34+
return (
35+
definition.kind === "OperationDefinition" &&
36+
definition.operation === "query"
37+
);
38+
},
39+
linkForOperationType("query"),
40+
linkForOperationType("mutation"),
41+
);
42+
};
43+
1444
const createClientWithoutWebsockets = function (opts) {
1545
const authLink = setContext(function (_, { headers }) {
1646
// get the authentication token from local storage if it exists
@@ -25,9 +55,7 @@ const createClientWithoutWebsockets = function (opts) {
2555
};
2656
});
2757

28-
const httpLink = createHttpLink({
29-
uri: opts.url,
30-
});
58+
const httpLink = mkHttpLink(opts);
3159

3260
return new ApolloClient({
3361
link: authLink.concat(httpLink),
@@ -40,13 +68,7 @@ const createClientWithoutWebsockets = function (opts) {
4068
};
4169

4270
const createClientWithWebsockets = function (opts) {
43-
const httpLink = new HttpLink({
44-
uri: opts.url,
45-
options: {
46-
authToken: opts.authToken,
47-
reconnect: true,
48-
},
49-
});
71+
const httpLink = mkHttpLink(opts);
5072

5173
const wsLink = new GraphQLWsLink(
5274
createWsClient({

‎src/GraphQL/Client/BaseClients/Apollo.purs‎

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
-- | Creates GraphQL Apollo clients
22
module GraphQL.Client.BaseClients.Apollo
33
( ApolloClientOptions
4+
, ApolloClientOptions'
45
, ApolloSubClientOptions
6+
, ApolloSubClientOptions'
57
, ApolloClient
68
, ApolloSubClient
79
, MutationOpts
810
, QueryOpts
911
, createClient
12+
, createClient'
1013
, createSubscriptionClient
14+
, createSubscriptionClient'
1115
, class IsApollo
1216
, updateCacheJson
1317
, updateCache
@@ -45,13 +49,37 @@ type ApolloClientOptions =
4549
, headers :: Array RequestHeader
4650
}
4751

52+
-- | As `ApolloClientOptions`, plus `operationTypeHeader`: when set, HTTP
53+
-- | requests include the GraphQL operation type ("query" or "mutation") as
54+
-- | the value of the header with this name, so eg. a load balancer can route
55+
-- | queries to a read replica.
56+
type ApolloClientOptions' =
57+
{ url :: URL
58+
, authToken :: Maybe String
59+
, headers :: Array RequestHeader
60+
, operationTypeHeader :: Maybe String
61+
}
62+
4863
type ApolloSubClientOptions =
4964
{ url :: URL
5065
, websocketUrl :: URL
5166
, authToken :: Maybe String
5267
, headers :: Array RequestHeader
5368
}
5469

70+
-- | As `ApolloSubClientOptions`, plus `operationTypeHeader`: when set, HTTP
71+
-- | requests include the GraphQL operation type ("query" or "mutation") as
72+
-- | the value of the header with this name, so eg. a load balancer can route
73+
-- | queries to a read replica. Subscriptions go over the websocket and are
74+
-- | unaffected (custom headers are not supported in websockets).
75+
type ApolloSubClientOptions' =
76+
{ url :: URL
77+
, websocketUrl :: URL
78+
, authToken :: Maybe String
79+
, headers :: Array RequestHeader
80+
, operationTypeHeader :: Maybe String
81+
}
82+
5583
-- | Apollo client to make graphQL queries and mutations.
5684
-- | From the @apollo/client npm module
5785
foreign import data ApolloClient :: Type
@@ -91,41 +119,60 @@ createClient
91119
:: forall schema
92120
. ApolloClientOptions
93121
-> Effect (Client ApolloClient schema)
94-
createClient = clientOptsToForeign >>> createClientImpl >>> map Client
122+
createClient { url, authToken, headers } =
123+
createClient' { url, authToken, headers, operationTypeHeader: Nothing }
124+
125+
createClient'
126+
:: forall schema
127+
. ApolloClientOptions'
128+
-> Effect (Client ApolloClient schema)
129+
createClient' = clientOptsToForeign >>> createClientImpl >>> map Client
95130

96131
createSubscriptionClient
97132
:: forall schema
98133
. ApolloSubClientOptions
99134
-> Effect (Client ApolloSubClient schema)
100-
createSubscriptionClient = clientOptsToForeign >>> createSubscriptionClientImpl >>> map Client
135+
createSubscriptionClient { url, websocketUrl, authToken, headers } =
136+
createSubscriptionClient' { url, websocketUrl, authToken, headers, operationTypeHeader: Nothing }
137+
138+
createSubscriptionClient'
139+
:: forall schema
140+
. ApolloSubClientOptions'
141+
-> Effect (Client ApolloSubClient schema)
142+
createSubscriptionClient' = clientOptsToForeign >>> createSubscriptionClientImpl >>> map Client
101143

102144
clientOptsToForeign
103145
:: forall r
104146
. { authToken :: Maybe String
105147
, headers :: Array RequestHeader
148+
, operationTypeHeader :: Maybe String
106149
| r
107150
}
108151
-> { authToken :: Nullable String
109152
, headers :: Object String
153+
, operationTypeHeader :: Nullable String
110154
| r
111155
}
112156
clientOptsToForeign opts =
113157
opts
114158
{ authToken = toNullable opts.authToken
115159
, headers = Object.fromFoldable $ opts.headers <#> \h -> Tuple (name h) (value h)
160+
, operationTypeHeader = toNullable opts.operationTypeHeader
116161
}
117162

118163
type ApolloClientOptionsForeign =
119164
{ url :: URL
120165
, authToken :: Nullable String
121166
, headers :: Object String
167+
, operationTypeHeader :: Nullable String
122168
}
123169

124170
type ApolloSubApolloClientOptionsForeign =
125171
{ url :: URL
126172
, websocketUrl :: URL
127173
, authToken :: Nullable String
128174
, headers :: Object String
175+
, operationTypeHeader :: Nullable String
129176
}
130177

131178
instance queryClient ::

0 commit comments

Comments
 (0)