This package contains utilities to create GraphQL clients for queries, mutations, and subscriptions. The clients provide authentication via cookies or a bearer token authorization header.
Install the client:
yarn add @moveaxlab/graphql-client
Install code generation packages from graphql-codegen
:
@graphql-codegen/cli
is the base package@graphql-codegen/add
adds custom code to generated files@graphql-codegen/schema-ast
allows to stitch together different schemas@graphql-codegen/typescript
and@graphql-codegen/typescript-operations
are used for type generation@graphql-codegen/fragment-matcher
allows handling of union types
overwrite: true
generates:
# generated schema file
./schema.graphql:
schema:
- ./schemas/common.graphql # add all the .graphql files you want to combine here
plugins:
- add: "# THIS IS A GENERATED FILE. DO NOT EDIT IT."
- schema-ast
# generated file with types
./src/api/graphql/__generated__/types.ts:
plugins:
- add: "/* eslint-disable */"
- add: "/* THIS IS A GENERATED FILE. DO NOT EDIT */"
- typescript
- typescript-operations
schema:
- ./schema.graphql # your combined, generated schema
documents:
- ./src/api/graphql/queries/*.ts # path to the files containing gql tags
config:
scalars:
Long: number # add any other custom scalar here
maybeValue: T | undefined
avoidOptionals: false
skipTypename: true
declarationKind: interface
flattenGeneratedTypes: true
# generated introspection results
./app/api/graphql/__generated__/introspection-result.ts:
schema:
- ./schema.graphql # your combined, generated schema
plugins:
- fragment-matcher
The introspection results configuration is only needed if your schema has interfaces/unions in it.
Both the query and mutation client and the subscription client handle authentication for you.
Authentication can be performed in one of two ways:
- using cookies (aka
cookies
) - using a bearer token authorization header (aka
headers
)
When using header authentication, you must provide to both clients a getAccessToekn
function that
returns the access token. The clients will use this function when making requests that need authentication.
Subscriptions are always authenticated.
In both types of authentication, you need to pass to the clients a isTokenExpired
function.
This function will be invoked when an error happens, and should return true
if the error indicates that the access token is expired.
You must implement the onTokenExpired
abstract method when extending the client.
This method will be called when a query or mutation fails for a token expired error.
Your token refresh logic must start when this method is called.
The token refresh logic must call the signalTokenRefreshed
or signalTokenRefreshFailed
methods of the client,
based on the outcome of the refresh operation.
The
onTokenExpired
method may be called multiple times, if several queries or mutations happen while the token is expired.
To create a query and mutation client, extend the GraphQLClient
class.
You must implement the abstract field errorMap
, and the abstract method onTokenExpired
.
The errorMap
field must be a dictionary mapping error codes to error classes.
When an error happens, if its error code matches something in the errorMap
, its matching error class is instantiated and thrown.
To implement a query or a mutation, use the createQuery
or createMutation
protected methods.
Both methods take in input a documentNode
(output of the gql
tag) as first parameters,
and some options as second parameter.
The possible options for queries and mutations are:
authenticated
, boolean, true if the query should be authenticated and should thus attempt to refresh the tokenconverter
, a function that converts the return type to something else
Both methods take three generics:
V
, the input typeR
, the output typeTR
, the converted output type (only needed if theconverter
options is defined)
class Client extends GraphQLClient {
myQuery = this.makeQuery<RequestType, ReturnType, ConvertedReturnType>(
query,
{
authenticated: true,
converter: (res: ReturnType): ConvertedReturnType => { /* ... */ },
}
);
}
To create a client to handle subscription extend the SubscriptionClient
class.
The subscription client only works with the graphql-ws
protocol.
The constructor take an object of type options.
Name | Required | Type | Description |
---|---|---|---|
host | true | string | URL of the GraphQL over WebSocket Protocol compliant server to connect. |
wsImpl | false | unknown | A custom WebSocket implementation to use instead of the one provided by the global scope. Mostly useful for when using the client outside of the browser environment. |
retryWait | false | (retries: number) => Promise | Control the wait time between retries. You may implement your own strategy by timing the resolution of the returned promise with the retries count. Retries argument counts actual connection attempts, so it will begin with 0 after the first retryable disconnect. |
The class should implement the following methods
connectionParams: () => Promise<Record<string, unknown>>
: Optional parameters, passed through the payload field with the ConnectionInit message, that the client specifies when establishing a connection with the server. You can use this for securely passing arguments for authentication.isTokenExpired: (event: CloseEvent) => boolean;
: Must return true if the CloseEvent means that the token is expired.onTokenExpired: () => void;
: Called when an error representing a TokenExpired happens.onError: (error: CloseEvent) => void;
: Called when a fatal error happens.dispatch: (event: unknown | null) => void;
: Called when an event is received.onConnecting: () => void;
: Called when socket is connectingonConnected: () => void;
: Called when socket is connectedonDisconnected: () => void;
: Called when socket is disconnectedlatencyMeasured: (latencyMs: number) => void;
: Used in order to debug the latency between client and socket
The signalReady
method must be called after initialization is completed.
If you need custom initialization logic, call signalReady
after it is complete, otherwise you can call signalReady
in your constructor.
To implement a subscription use the makeSubscription
protected method. The method take in input an object with the followgin keys:
Name | Type | Description |
---|---|---|
subscription | documentNode |
The output of the gql tag |
name | string | The name of the subscription. This must be unique. |
eventCreator | (event: R) => TR | null; |
A method that handle the received payload from the subscription |
This method take three generics:
V
, the input typeR
, The output typeTR
, the converted output type (if not present it's equal toR
)
class Client extends SubscriptionClient {
//... class implementation here
subscription = this.makeSubscription<
SubscriptionInput,
SubscriptionReceivedPayload,
SubscriptionConvertedRecevidedPaylod
>({
subscription: gqlSubscription,
name: 'subscriptionName',
eventCreator: (res: SubscriptionReceivedPayload) : SubscriptionConvertedRecevidedPaylod => {/*...*/},
});
}
Before activating a subscription, you must connect the subscription client, by calling the connect
method.
If your subscription server requires authentication, you must wait for your authentication token to be available before calling connect
.
Any subscriptions started before calling the connect
method will block until the connection has been established.
To subscribe and unsubscribe use:
const client = new Client();
client.subscription.subscribe(input);
client.subscription.unsubscribe(input);
Subscriptions are kept between socket connection and reconnection.
Calling the disconnect
method on the subscription client will cancel all subscriptions.
The getActiveSubscriptions
method returns a list of the currently active subscription, as a list of subscription names and parameters.
You can rely on this method to implement custom fallback logic if the socket is disconnected.
Socket connection and disconnection is signaled through the onConnected
and onDisconnected
methods.