Skip to main content

Miscellaneous

GraphQLResult

GraphQL Specification

The returned GraphQLResult is the output of the execution of a GraphQL request it contains the encountered GraphQLErrors, 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 a Map<String, Object?>? for Queries and Mutations or a Stream<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, the data property will not be set in the GraphQLResult.toJson Map following the spec.

  • The errors contain the GraphQLErrors 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 the GraphQLResult.data property will be null).

  • The extensions is a Map<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. Most GraphQLExtensions modify this values to provide additional functionalities. The keys for the extensions Map should be unique, you may want to prefix them with an identifier such as a package name.

ScopedMap

Source code

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) ScopedHolders 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: