graphql 4.0.0-alpha.10
graphql: ^4.0.0-alpha.10 copied to clipboard
A stand-alone GraphQL client for Dart, bringing all the features from a modern GraphQL client to one easy to use package.
GraphQL Client #
graphql/client.dart
is a GraphQL client for dart modeled on the apollo client, and is currently the most popular GraphQL client for dart. It is co-developed alongside graphql_flutter
on github, where you can find more in-depth examples. We also have a lively community alongside the rest of the GraphQL Dart community on discord.
As of v4
, it is built on foundational libraries from the gql-dart project, including gql
, gql_link
, and normalize
. We also depend on hive for persistence via HiveStore
.
- GraphQL Client
Useful API Docs:
GraphQLCache
GraphQLDataProxy
API docs (direct cache access)
Installation #
First, depend on this package:
dependencies:
graphql: ^4.0.0-beta
And then import it inside your dart code:
import 'package:graphql/client.dart';
Migration Guide #
Find the migration from version 3 to version 4 here.
Basic Usage #
To connect to a GraphQL Server, we first need to create a GraphQLClient
. A GraphQLClient
requires both a cache
and a link
to be initialized.
In our example below, we will be using the Github Public API. we are going to use HttpLink
which we will concatenate with AuthLink
so as to attach our github access token.
For the cache, we are going to use GraphQLCache
.
// ...
final _httpLink = HttpLink(
'https://api.github.com/graphql',
);
final _authLink = AuthLink(
getToken: () async => 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN',
);
Link _link = _authLink.concat(_httpLink);
/// subscriptions must be split otherwise `HttpLink` will. swallow them
if (websocketEndpoint != null){
final _wsLink = WebSocketLink(websockeEndpoint);
_link = Link.split((request) => request.isSubscription, _wsLink, _link);
}
final GraphQLClient client = GraphQLClient(
/// **NOTE** The default store is the InMemoryStore, which does NOT persist to disk
cache: GraphQLCache(),
link: _link,
);
// ...
Persistence #
In v4
, GraphQLCache
is decoupled from persistence, which is managed (or not) by its store
argument.
We provide a HiveStore
for easily using hive boxes as storage,
which requires a few changes to the above:
NB: This is different in
graphql_flutter
, which providesawait initHiveForFlutter()
for initialization inmain
GraphQL getClient() async {
...
/// initialize Hive and wrap the default box in a HiveStore
final store = await HiveStore.open(path: 'my/cache/path');
return GraphQLClient(
/// pass the store to the cache for persistence
cache: GraphQLCache(store: store),
link: _link,
);
}
Once you have initialized a client, you can run queries and mutations.
Query #
Creating a query is as simple as creating a multiline string:
const String readRepositories = r'''
query ReadRepositories($nRepositories: Int!) {
viewer {
repositories(last: $nRepositories) {
nodes {
__typename
id
name
viewerHasStarred
}
}
}
}
''';
Then create a QueryOptions
object:
NB: for
document
- Use our built-in help function -gql(query)
to convert your document string to ASTsdocument
.
In our case, we need to pass nRepositories
variable and the document name is readRepositories
.
const int nRepositories = 50;
final QueryOptions options = QueryOptions(
document: gql(readRepositories),
variables: <String, dynamic>{
'nRepositories': nRepositories,
},
);
And finally you can send the query to the server and await
the response:
// ...
final QueryResult result = await client.query(options);
if (result.hasException) {
print(result.exception.toString());
}
final List<dynamic> repositories =
result.data['viewer']['repositories']['nodes'] as List<dynamic>;
// ...
Mutations #
Creating a mutation is similar to creating a query, with a small difference. First, start with a multiline string:
const String addStar = r'''
mutation AddStar($starrableId: ID!) {
action: addStar(input: {starrableId: $starrableId}) {
starrable {
viewerHasStarred
}
}
}
''';
Then instead of the QueryOptions
, for mutations we will MutationOptions
, which is where we pass our mutation and id of the repository we are starring.
// ...
final MutationOptions options = MutationOptions(
document: gql(addStar),
variables: <String, dynamic>{
'starrableId': repositoryID,
},
);
// ...
And finally you can send the mutation to the server and await
the response:
// ...
final QueryResult result = await client.mutate(options);
if (result.hasException) {
print(result.exception.toString());
return;
}
final bool isStarred =
result.data['action']['starrable']['viewerHasStarred'] as bool;
if (isStarred) {
print('Thanks for your star!');
return;
}
// ...
GraphQL Upload
gql_http_link provides support for the GraphQL Upload spec as proposed at https://github.com/jaydenseric/graphql-multipart-request-spec
mutation($files: [Upload!]!) {
multipleUpload(files: $files) {
id
filename
mimetype
path
}
}
import "package:http/http.dart" show Multipartfile;
// ...
final myFile = MultipartFile.fromString(
"",
"just plain text",
filename: "sample_upload.txt",
contentType: MediaType("text", "plain"),
);
final result = await graphQLClient.mutate(
MutationOptions(
document: gql(uploadMutation),
variables: {
'files': [myFile],
},
)
);
Subscriptions #
To use subscriptions, a subscription-consuming link must be split from your HttpLink
or other terminating link route:
link = Link.split((request) => request.isSubscription, websocketLink, link);
Then you can subscribe
to any subscription
s provided by your server schema:
final subscriptionDocument = gql(
r'''
subscription reviewAdded {
reviewAdded {
stars, commentary, episode
}
}
''',
);
// graphql/client.dart usage
subscription = client.subscribe(
SubscriptionOptions(
document: subscriptionDocument
),
);
subscription.listen(reactToAddedReview)
client.watchQuery
and ObservableQuery
#
client.watchQuery
can be used to execute both queries and mutations, then reactively listen to changes to the underlying data in the cache. It is used in the Query
and Mutation
widgets of graphql_flutter
:
final observableQuery = client.watchQuery(
WatchQueryOptions(
document: gql(
r'''
query HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
name
}
}
''',
),
variables: {'ep': 'NEWHOPE'},
),
);
/// Listen to the stream of results. This will include:
/// * `options.optimisitcResult` if passed
/// * The result from the server (if `options.fetchPolicy` includes networking)
/// * rebroadcast results from edits to the cache
observableQuery.stream.listen((QueryResult result) {
if (!result.isLoading && result.data != null) {
if (result.hasException) {
print(result.exception);
return;
}
if (result.isLoading) {
print('loading');
return;
}
doSomethingWithMyQueryResult(myCustomParser(result.data));
}
});
// ... cleanup:
observableQuery.close();
ObservableQuery
is a bit of a kitchen sink for reactive operation logic – consider looking at the API docs if you'd like to develop a deeper understanding.
NB:
watchQuery
andObservableQuery
currently don't have a nice APIs forupdate
onCompleted
andonError
callbacks, but you can have a look at howgraphql_flutter
registers them throughonData
inMutation.runMutation
.
Direct Cache Access API #
The GraphQLCache
leverages normalize
to give us a fairly apollo-ish direct cache access API, which is also available on GraphQLClient
.
This means we can do local state management in a similar fashion as well.
A complete and well-commented rundown of can be found in the
GraphQLDataProxy
API docs
NB You likely want to call the cache access API from your
client
for automatic broadcasting support.
Policies #
Policies are used to configure execution and error behavior for a given request.
The client's default policies can also be set for each method via the defaultPolicies
option.
FetchPolicy
determines where the client may return a result from.
Possible options:
- cacheFirst (default): return result from cache. Only fetch from network if cached result is not available.
- cacheAndNetwork: return result from cache first (if it exists), then return network result once it's available.
- cacheOnly: return result from cache if available, fail otherwise.
- noCache: return result from network, fail if network call doesn't succeed, don't save to cache.
- networkOnly: return result from network, fail if network call doesn't succeed, save to cache.
ErrorPolicy
determines the level of events for errors in the execution result.
Possible options:
- none (default): Any GraphQL Errors are treated the same as network errors and any data is ignored from the response.
- ignore: Ignore allows you to read any data that is returned alongside GraphQL Errors, but doesn't save the errors or report them to your UI.
- all: Using the all policy is the best way to notify your users of potential issues while still showing as much data as possible from your server. It saves both data and errors into the Apollo Cache so your UI can use them.
Exceptions #
If there were problems encountered during a query or mutation, the QueryResult
will have an OperationException
in the exception
field:
/// Container for both [graphqlErrors] returned from the server
/// and any [linkException] that caused a failure.
class OperationException implements Exception {
/// Any graphql errors returned from the operation
List<GraphQLError> graphqlErrors = [];
/// Errors encountered during execution such as network or cache errors
LinkException linkException;
}
Example usage:
if (result.hasException) {
if (result.exception.linkException is NetworkException) {
// handle network issues, maybe
}
return Text(result.exception.toString())
}
Links #
graphql
and graphql_flutter
now use the gql_link
system, re-exporting
gql_http_link,
gql_error_link,
gql_dedupe_link,
and the api from gql_link,
as well as our own custom WebSocketLink
and AuthLink
.
Composing Links #
NB:
WebSocketLink
and other "terminating links" must be used withsplit
when there are multiple terminating links.
The gql_link
systm has a well-specified routing system:
a rundown of the composition api:
// kitchen sink:
Link.from([
// common links run before every request
DedupeLink(), // dedupe requests
ErrorLink(onException: reportClientException),
]).split( // split terminating links, or they will break
(request) => request.isSubscription,
MyCustomSubscriptionAuthLink().concat(
WebSocketLink(mySubscriptionEndpoint),
), // MyCustomSubscriptionAuthLink is only applied to subscriptions
AuthLink(getToken: httpAuthenticator).concat(
HttpLink(myAppEndpoint),
)
);
// adding links after here would be pointless, as they would never be accessed
/// both `Link.from` and `link.concat` can be used to chain links:
final Link _link = _authLink.concat(_httpLink);
final Link _link = Link.from([_authLink, _httpLink]);
/// `Link.split` and `link.split` route requests to the left or right based on some condition
/// for instance, if you do `authLink.concat(httpLink).concat(websocketLink)`,
/// `websocketLink` won't see any `subscriptions`
link = Link.split((request) => request.isSubscription, websocketLink, link);
When combining links, it isimportant to note that:
- Terminating links like
HttpLink
andWebsocketLink
must come at the end of a route, and will not call links following them. - Link order is very important. In
HttpLink(myEndpoint).concat(AuthLink(getToken: authenticate))
, theAuthLink
will never be called.
AWS AppSync Support #
Cognito Pools
To use with an AppSync GraphQL API that is authorized with AWS Cognito User Pools, simply pass the JWT token for your Cognito user session in to the AuthLink
:
// Where `session` is a CognitorUserSession
// from amazon_cognito_identity_dart_2
final token = session.getAccessToken().getJwtToken();
final AuthLink authLink = AuthLink(
getToken: () => token,
);
See more: Issue #209
Other Authorization Types
API key, IAM, and Federated provider authorization could be accomplished through custom links, but it is not known to be supported. Anyone wanting to implement this can reference AWS' JS SDK AuthLink
implementation.
- Making a custom link: Comment on Issue 173
- AWS JS SDK
auth-link.ts
: aws-mobile-appsync-sdk-js:auth-link.ts
Parsing ASTs at build-time #
All document
arguments are DocumentNode
s from gql/ast
.
We supply a gql
helper for parsing, them, but you can also
parse documents at build-time use ast_builder
from
package:gql_code_gen
:
dev_dependencies:
gql_code_gen: ^0.1.5
add_star.graphql
:
mutation AddStar($starrableId: ID!) {
action: addStar(input: { starrableId: $starrableId }) {
starrable {
viewerHasStarred
}
}
}
import 'package:gql/add_star.ast.g.dart' as add_star;
// ...
final MutationOptions options = MutationOptions(
document: add_star.document,
variables: <String, dynamic>{
'starrableId': repositoryID,
},
);
// ...
PersistedQueriesLink
(experimental) ⚠️ OUT OF SERVICE ⚠️ #
NOTE: There is a PR for migrating the v3
PersistedQueriesLink
, and it works, but requires more consideration. It will be fixed before v4
stable
is published
To improve performance you can make use of a concept introduced by apollo called Automatic persisted queries (or short "APQ") to send smaller requests and even enabled CDN caching for your GraphQL API.
ATTENTION: This also requires you to have a GraphQL server that supports APQ, like Apollo's GraphQL Server and will only work for queries (but not for mutations or subscriptions).
You can than use it simply by prepending a PersistedQueriesLink
to your normal HttpLink
:
final PersistedQueriesLink _apqLink = PersistedQueriesLink(
// To enable GET queries for the first load to allow for CDN caching
useGETForHashedQueries: true,
);
final HttpLink _httpLink = HttpLink(
'https://api.url/graphql',
);
final Link _link = _apqLink.concat(_httpLink);