Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 33 additions & 11 deletions src/GraphQL/Client/BaseClients/Apollo.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { createClient as createWsClient } from "graphql-ws";
import {
gql,
split,
HttpLink,
createHttpLink,
InMemoryCache,
ApolloClient,
Expand All @@ -11,6 +10,37 @@ import { getMainDefinition } from "@apollo/client/utilities/index.js";
import { setContext } from "@apollo/client/link/context/index.js";
import { GraphQLWsLink } from "@apollo/client/link/subscriptions/index.js";

// Build the HTTP link. When opts.operationTypeHeader is set, requests are
// split by operation type and the type ("query" or "mutation") is sent as
// the value of that header, so eg. a load balancer can route queries to a
// read replica.
const mkHttpLink = function (opts) {
if (!opts.operationTypeHeader) {
return createHttpLink({
uri: opts.url,
});
}

const linkForOperationType = function (operationType) {
return createHttpLink({
uri: opts.url,
headers: { [opts.operationTypeHeader]: operationType },
});
};

return split(
function ({ query }) {
const definition = getMainDefinition(query);
return (
definition.kind === "OperationDefinition" &&
definition.operation === "query"
);
},
linkForOperationType("query"),
linkForOperationType("mutation"),
);
};

const createClientWithoutWebsockets = function (opts) {
const authLink = setContext(function (_, { headers }) {
// get the authentication token from local storage if it exists
Expand All @@ -25,9 +55,7 @@ const createClientWithoutWebsockets = function (opts) {
};
});

const httpLink = createHttpLink({
uri: opts.url,
});
const httpLink = mkHttpLink(opts);

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

const createClientWithWebsockets = function (opts) {
const httpLink = new HttpLink({
uri: opts.url,
options: {
authToken: opts.authToken,
reconnect: true,
},
});
const httpLink = mkHttpLink(opts);

const wsLink = new GraphQLWsLink(
createWsClient({
Expand Down
51 changes: 49 additions & 2 deletions src/GraphQL/Client/BaseClients/Apollo.purs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
-- | Creates GraphQL Apollo clients
module GraphQL.Client.BaseClients.Apollo
( ApolloClientOptions
, ApolloClientOptions'
, ApolloSubClientOptions
, ApolloSubClientOptions'
, ApolloClient
, ApolloSubClient
, MutationOpts
, QueryOpts
, createClient
, createClient'
, createSubscriptionClient
, createSubscriptionClient'
, class IsApollo
, updateCacheJson
, updateCache
Expand Down Expand Up @@ -45,13 +49,37 @@ type ApolloClientOptions =
, headers :: Array RequestHeader
}

-- | As `ApolloClientOptions`, plus `operationTypeHeader`: when set, HTTP
-- | requests include the GraphQL operation type ("query" or "mutation") as
-- | the value of the header with this name, so eg. a load balancer can route
-- | queries to a read replica.
type ApolloClientOptions' =
{ url :: URL
, authToken :: Maybe String
, headers :: Array RequestHeader
, operationTypeHeader :: Maybe String
}

type ApolloSubClientOptions =
{ url :: URL
, websocketUrl :: URL
, authToken :: Maybe String
, headers :: Array RequestHeader
}

-- | As `ApolloSubClientOptions`, plus `operationTypeHeader`: when set, HTTP
-- | requests include the GraphQL operation type ("query" or "mutation") as
-- | the value of the header with this name, so eg. a load balancer can route
-- | queries to a read replica. Subscriptions go over the websocket and are
-- | unaffected (custom headers are not supported in websockets).
type ApolloSubClientOptions' =
{ url :: URL
, websocketUrl :: URL
, authToken :: Maybe String
, headers :: Array RequestHeader
, operationTypeHeader :: Maybe String
}

-- | Apollo client to make graphQL queries and mutations.
-- | From the @apollo/client npm module
foreign import data ApolloClient :: Type
Expand Down Expand Up @@ -91,41 +119,60 @@ createClient
:: forall schema
. ApolloClientOptions
-> Effect (Client ApolloClient schema)
createClient = clientOptsToForeign >>> createClientImpl >>> map Client
createClient { url, authToken, headers } =
createClient' { url, authToken, headers, operationTypeHeader: Nothing }

createClient'
:: forall schema
. ApolloClientOptions'
-> Effect (Client ApolloClient schema)
createClient' = clientOptsToForeign >>> createClientImpl >>> map Client

createSubscriptionClient
:: forall schema
. ApolloSubClientOptions
-> Effect (Client ApolloSubClient schema)
createSubscriptionClient = clientOptsToForeign >>> createSubscriptionClientImpl >>> map Client
createSubscriptionClient { url, websocketUrl, authToken, headers } =
createSubscriptionClient' { url, websocketUrl, authToken, headers, operationTypeHeader: Nothing }

createSubscriptionClient'
:: forall schema
. ApolloSubClientOptions'
-> Effect (Client ApolloSubClient schema)
createSubscriptionClient' = clientOptsToForeign >>> createSubscriptionClientImpl >>> map Client

clientOptsToForeign
:: forall r
. { authToken :: Maybe String
, headers :: Array RequestHeader
, operationTypeHeader :: Maybe String
| r
}
-> { authToken :: Nullable String
, headers :: Object String
, operationTypeHeader :: Nullable String
| r
}
clientOptsToForeign opts =
opts
{ authToken = toNullable opts.authToken
, headers = Object.fromFoldable $ opts.headers <#> \h -> Tuple (name h) (value h)
, operationTypeHeader = toNullable opts.operationTypeHeader
}

type ApolloClientOptionsForeign =
{ url :: URL
, authToken :: Nullable String
, headers :: Object String
, operationTypeHeader :: Nullable String
}

type ApolloSubApolloClientOptionsForeign =
{ url :: URL
, websocketUrl :: URL
, authToken :: Nullable String
, headers :: Object String
, operationTypeHeader :: Nullable String
}

instance queryClient ::
Expand Down
20 changes: 20 additions & 0 deletions src/GraphQL/Client/Query.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const formatError = (err) => {
if (!err) return "";
const props = ["message", "code", "syscall", "hostname", "address", "port"]
.filter((k) => err[k])
.map((k) => `${k}: ${err[k]}`);
if (err.cause) props.push(`cause: ${formatError(err.cause)}`);
return props.join(", ");
};

export const cause = (err) => {
const c = err.cause;
if (!c) return "";
if (c instanceof AggregateError) {
return [
c.message,
...Array.from(c.errors, (e, i) => `[${i}] ${formatError(e)}`),
].join("\n");
}
return formatError(c);
};
4 changes: 4 additions & 0 deletions src/GraphQL/Client/Query.purs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import GraphQL.Client.Types (class GqlQuery, class QueryClient, Client(..), GqlR
import GraphQL.Client.Variables (class VarsTypeChecked, getVarsJson, getVarsTypeNames)
import Type.Proxy (Proxy(..))

foreign import cause :: Error -> String

-- | Run a graphQL query with a custom decoder and custom options
queryOptsWithDecoder
:: forall client directives schema query returns queryOpts mutationOpts sr
Expand Down Expand Up @@ -261,6 +263,8 @@ addErrorInfo schema queryName q =
<> show queryName
<> ".\nerror: "
<> message err
<> ".\ncause: "
<> cause err
<> ".\nquery: "
<> queryName
<> " "
Expand Down
Loading