Miscellaneous
GraphQLResult
The returned GraphQLResult
is the output of the execution of a GraphQL request it contains the encountered GraphQLError
s, the output extensions
and the data
payload. The GraphQLResult.toJson
Map is used by package:leto_shelf
when constructing an HTTP response's body.
The
data
is aMap<String, Object?>?
for Queries and Mutations or aStream<GraphQLResult>
for subscriptions. It has the payload returned by the resolvers during execution. Will be null if there was an error in validation or in the execution of a non-nullable root field. If there was an error in validation, thedata
property will not be set in theGraphQLResult.toJson
Map following the spec.The
errors
contain theGraphQLError
s encountered during validation or execution. If a resolver throws an error, it will appear in this error list. If the field's return type is nullable, a null value will be set as the output for that field. If the type is non-nullable the resolver will continue to throw an exception until a nullable field is reached or the root resolver is reached (in this case theGraphQLResult.data
property will be null).The
extensions
is aMap<String, Object?>?
with custom values that you may want to provide to the client. All values should be serializable since they may be returned as part of an HTTP response. MostGraphQLExtensions
modify this values to provide additional functionalities. The keys for theextensions
Map should be unique, you may want to prefix them with an identifier such as a package name.
ScopedMap
An ScopedMap
allows you to pass and use dependencies or services within your resolvers or extensions. It consists of multiple maps, one for each scope, and a set of immutable references (or keys) with overridable defaults.
To retrieve a value from a reference, the map checks whether a value was already instantiated for the scope or in any of its parents. If it has not been instantiated, the default is returned and saved in the scope.
This forms a tree of scopes, where one node scope has access to its parent values.
To override the value of a reference for a given scope you instantiate a ScopedMap
with the values to override, if it is a child, you can pass the parent as a parameter to the constructor.
ScopedHolder
A ScopedHolder
is simply an object that contains a ScopedMap get globals;
getter. This map represents the scope associated with the object. As discussed in the Request Contexts section, all contexts are (implement) ScopedHolder
s and therefore have access to the values in the scope.
ScopedRef
You can specify the behavior and the default values of references using ScopedRef
. As explained in the source code docs, a "global" ref will instantiate a value accessible to all scopes in a scope tree. A "local" ref will instantiate the value (and make it accessible) only to children scopes in which the value is instantiated.
Example usage
Example usage with the GraphQL
executor and different ways to override the values is shown in the following code snippet:
final ScopedRef<int> ref = ScopedRef.global((ScopedMap scope) => 4);
final schema = GraphQLSchema(
queryObject: objectType(
'Query',
fields: [
graphQLint.field('fieldName', (ctx) => ref.get(ctx)),
],
),
);
final executorWithDefault = GraphQL(schema);
var result = await executorWithDefault.parseAndExecute('{fieldName}');
var data = result.data as Map<String, Object?>;
assert(data['fieldName'] == 4);
result = await executorWithDefault.parseAndExecute(
'{fieldName}',
globalVariables: {
ref: 6,
},
);
data = result.data as Map<String, Object?>;
assert(data['fieldName'] == 6);
final executorWithOverride = GraphQL(
schema,
globalVariables: ScopedMap(
{
ref: 5,
},
),
);
result = await executorWithOverride.parseAndExecute('{fieldName}');
data = result.data as Map<String, Object?>;
assert(data['fieldName'] == 5);
Error Handling
One typically has multiple options to represent and let the client know that there was an error in the request.
If using HTTP (or WebSockets) fatal errors such as a malformed query string are already handled and follow the spec in each case.
Exceptions and GraphQLError
If an error does not require a different type to be expressed and a more implicit approach is preferable, perhaps for errors that happen in most endpoints (authentication, authorization, input validation), one can use exceptions and send the necessary information through custom extensions in the payload.
()
Future<int> userChats(Ctx ctx) async {
final user = await userFromCtx(ctx);
if (user == null) {
throw GraphQLError(
'This endpoint requires an authenticated user.', // message
extensions: {
'appError': {
'code': 'UNAUTHENTICATED',
},
},
// You can also pass a `sourceError` and `stackTrace` if the given error
// was generated from an exception
// sourceError,
// stackTrace,
);
}
// You could also throw a list of errors with GraphQLException
final errors = [
if (!user.emailVerified)
GraphQLError(
'This functionality requires that you verify your email.', // message
extensions: {
'appError': {
'code': 'UNVERIFIED_EMAIL',
},
},
),
if (!user.canReadUserChats)
GraphQLError(
'You do not have access to this functionality.', // message
extensions: {
'appError': {
'code': 'UNAUTHORIZED',
},
},
),
];
if (errors.isNotEmpty) throw GraphQLException(errors);
// AUTHORIZED, now get the userChats
}
Of course, this could be abstracted and structured in a better way. For example, the "UNAUTHENTICATED" error could be a constant or it could be thrown inside userFromCtx(ctx)
call. The appError
key could be anything you want, but is has to be unique to avoid overriding other extensions.
Result types
- Result
"""
SomethingT! when the operation was successful or SomethingE! when an error was encountered.
"""
type ResultSomethingTSomethingE {
ok: SomethingT
err: SomethingE
isOk: Boolean!
}
- ResultU
"""
SomethingT when the operation was successful or SomethingE when an error was encountered.
"""
union ResultUSomethingTSomethingE = SomethingT | SomethingE
Error lists and interfaces
The error in the result union or object could be a simple object specific to the resolver. However, it could also by an union, an object that implements an "ApplicationError" interface or a list of errors, where the errors could also be of union type or objects that implement interfaces, or both. For a more thorough discussion on the topic, this guide to GraphQL errors may help you.
Hot Reload and Cycles
Since type and field schema definitions should probably be reused, this may pose a conflict to the beautifully hot reload capabilities of Dart. The cached instances will not change unless you execute the more expensive hot restart, which may also cause you to lose other state when developing.
Because of this, we provide an utility class HotReloadableDefinition
that handles definition caching, helps with cycles in instantiation and controls the re-instantiation of values. It receives a create
function that should return a new instance of the value. This value will be cached and reused throughout the schema's construction. To retrieve the current instance you can use the HotReloadableDefinition.value
getter.
The provided create
function receives a setValue
callback that should be called right after the instance's creation (with the newly constructed instance as argument), this is only necessary if the instance definition may contain cycles.
To re-instantiate all values that use HotReloadableDefinition
you can execute the static HotReloadableDefinition.incrementCounter
which will invalidate previously created instances, if you call HotReloadableDefinition.value
again, a new instance will be created with the, potentially new, hot reloaded code.
When using code generation all schema definitions use the HotReloadableDefinition
class to create type and field instances, you only need to call the generated recreateGraphQLApiSchema
function to instantiate the GraphQLSchema
each time the application hot reloads.
You can use other packages to hot reload the dart virtual machine (vm), for example:
- If using shelf you may want to try https://pub.dev/packages/shelf_hotreload. Most shelf examples in this repository already use this package.
- You could also search in https://pub.dev or try https://pub.dev/packages/hotreloader, which is used by
package:shelf_hotreload
.