Dart GraphQL server implementation. Utilities, code generator, examples and reference implementation.

Overview

Logo

Leto - GraphQL Server

A complete implementation of the official GraphQL specification in the Dart programming language.

Inspired by graphql-js, async-graphql and type-graphql. First version of the codebase was forked from angel-graphql. Many tests and utilities (DataLoader, printSchema) were ported from graphql-js.

Table of Contents

Quickstart

This provides a simple introduction to Leto, you can explore more in the following sections of this README or by looking at the tests, documentation and examples for each package. A fullstack Dart example with Flutter client and Leto/Shelf server can be found in https://github.com/juancastillo0/leto/tree/main/chat_example

The source code for this quickstart can be found in https://github.com/juancastillo0/leto/blob/main/leto_shelf/example/lib/quickstart_server.dart.

Install

Add dependencies to your pubspec.yaml

dependencies:
  leto_schema: ^0.0.1
  leto: ^0.0.1
  leto_shelf: ^0.0.1
  shelf: ^1.0.0
  shelf_router: ^1.0.0
  # Not nessary for the server, just for testing it
  http: ^1.0.0

dev_dependencies:
  # Only if you use code generation
  leto_generator: ^0.0.1
  build_runner: ^1.0.0

Create a GraphQLSchema

Specify the logic for your server, this could be anything such as accessing a database, reading a file or sending and http request. We will use a controller class with a stream that emits events on mutation to support subscriptions.

// this annotations is only necessary for code generation
@GraphQLClass()
class Model {
  final String state;
  final DateTime createdAt;

  const Model(this.state, this.createdAt);
}

/// Set up your state.
/// This could be anything such as a database connection
final stateRef = RefWithDefault<ModelController>.global(
  (scope) => ModelController(
    Model('InitialState', DateTime.now()),
  ),
);

class ModelController {
  Model? _value;
  Model? get value => _value;

  final _streamController = StreamController<Model>.broadcast();
  Stream<Model> get stream => _streamController.stream;

  ModelController(this._value);

  void setValue(Model newValue) {
    if (newValue.state == 'InvalidState') {
      // This will appear as an GraphQLError in the response.
      // You can pass more information using custom extensions.
      throw GraphQLError(
        "Can't be in InvalidState.",
        extensions: {'errorCodeExtension': 'INVALID_STATE'},
      );
    }
    _value = newValue;
    _streamController.add(newValue);
  }
}

With the logic that you want to expose, you can create the GraphQLSchema instance and access the controller state using the Ctx for each resolver and the RefWithDefault.get method. This is a schema with Query, Mutation and Subscription with a simple model. However, GraphQL is a very expressive language with Unions, Enums, complex Input Objects, collections and more. For more documentation on writing GraphQL Schemas with Leto you can read the following sections, tests and examples for each package. // TODO: more docs in the code

/// Create a [GraphQLSchema]
GraphQLSchema makeGraphQLSchema() {
  final GraphQLObjectType<Model> modelGraphQLType = objectType<Model>(
    'Model',
    fields: [
      graphQLString.nonNull().field(
            'state',
            resolve: (Model model, Ctx ctx) => model.state,
          ),
      graphQLDate.nonNull().field(
            'createdAt',
            resolve: (Model model, Ctx ctx) => model.createdAt,
          ),
    ],
  );
  final schema = GraphQLSchema(
    queryType: objectType('Query', fields: [
      modelGraphQLType.field(
        'getState',
        description: 'Get the current state',
        resolve: (Object? rootValue, Ctx ctx) => stateRef.get(ctx).value,
      ),
    ]),
    mutationType: objectType('Mutation', fields: [
      graphQLBoolean.nonNull().field(
        'setState',
        inputs: [
          GraphQLFieldInput(
            'newState',
            graphQLString.nonNull(),
            description: "The new state, can't be 'WrongState'!.",
          ),
        ],
        resolve: (Object? rootValue, Ctx ctx) {
          final newState = ctx.args['newState']! as String;
          if (newState == 'WrongState') {
            return false;
          }
          stateRef.get(ctx).setValue(Model(newState, DateTime.now()));
          return true;
        },
      ),
    ]),
    subscriptionType: objectType('Subscription', fields: [
      modelGraphQLType.nonNull().field(
            'onStateChange',
            subscribe: (Object? rootValue, Ctx ctx) => stateRef.get(ctx).stream,
          )
    ]),
  );
  assert(schema.schemaStr == schemaString.trim());
  return schema;
}

This will represent the following GraphQL Schema definition:

type Query {
  """Get the current state"""
  getState: Model
}

type Model {
  state: String!
  createdAt: Date!
}

"""An ISO-8601 Date."""
scalar Date

type Mutation {
  setState(
    """The new state, can't be 'WrongState'!."""
    newState: String!
  ): Boolean!
}

type Subscription {
  onStateChange: Model!
}

You can use code generation to create a function similar to makeGraphQLSchema with the following resolver definitions with annotations.

/// Code Generation
/// Using leto_generator, [makeGraphQLSchema] could be generated
/// with the following annotated functions and the [GraphQLClass]
/// annotation over [Model]

/// Get the current state
@Query()
Model? getState(Ctx ctx) {
  return stateRef.get(ctx).value;
}

@Mutation()
bool setState(
  Ctx ctx,
  // The new state, can't be 'WrongState'!.
  String newState,
) {
  if (newState == 'WrongState') {
    return false;
  }

  stateRef.get(ctx).setValue(Model(newState, DateTime.now()));
  return true;
}

@Subscription()
Stream<Model> onStateChange(Ctx ctx) {
  return stateRef.get(ctx).stream;
}

This generates the same modelGraphQLType in <file>.g.dart and graphqlApiSchema in 'lib/graphql_api.schema.dart' (TODO: configurable). The documentation comments will be used as description in the generated schema. More information on code generation can be found in the following sections, in the package:leto_generator's README or in the code generation example.

Start the server

With the GraphQLSchema and the resolver logic implemented, we can set up the shelf handlers for each route. In this case we will use the graphQLHttp handlers for the "/graphql" endpoint and graphQLWebSocket for "/graphql-subscription" which supports subscriptions. You could provide custom extensions, document validations or a ScopedMap to override the state in the GraphQL executor constructor.

Future<HttpServer> runServer({int? serverPort, ScopedMap? globals}) async {
  // you can override state with ScopedMap.setGlobal/setScoped
  final ScopedMap scopedMap = globals ?? ScopedMap.empty();
  if (globals == null) {
    // if it wasn't overridden it should be the default
    assert(stateRef.get(scopedMap).value?.state == 'InitialState');
  }
  // Instantiate the GraphQLSchema
  final schema = makeGraphQLSchema();
  // Instantiate the GraphQL executor, you can pass extensions and
  // decide whether you want to introspect the schema
  // and validate the requests
  final letoGraphQL = GraphQL(
    schema,
    extensions: [],
    introspect: true,
    globalVariables: scopedMap,
  );

  final port =
      serverPort ?? const int.fromEnvironment('PORT', defaultValue: 8080);
  const graphqlPath = 'graphql';
  const graphqlSubscriptionPath = 'graphql-subscription';
  final endpoint = 'http://localhost:$port/$graphqlPath';
  final subscriptionEndpoint = 'ws://localhost:$port/$graphqlSubscriptionPath';

  // Setup server endpoints
  final app = Router();
  // GraphQL HTTP handler
  app.all(
    '/$graphqlPath',
    graphQLHttp(letoGraphQL),
  );
  // GraphQL WebSocket handler
  app.all(
    '/$graphqlSubscriptionPath',
    graphQLWebSocket(
      letoGraphQL,
      pingInterval: const Duration(seconds: 10),
      validateIncomingConnection: (
        Map<String, Object?>? initialPayload,
        GraphQLWebSocketServer wsServer,
      ) {
        if (initialPayload != null) {
          // you can authenticated an user with the initialPayload:
          // final token = initialPayload['token']! as String;
          // ...
        }
        return true;
      },
    ),
  );

In the shelf router you can specify other handlers such as static files or other utilities. In the following code we set up a GraphQL UI explorer in the "/playground" route using the playgroundHandler handler and a "/graphql-schema" endpoint that returns the GraphQL schema String in the body of the response.

  // GraphQL schema and endpoint explorer web UI.
  // Available UI handlers: playgroundHandler, graphiqlHandler and altairHandler
  app.get(
    '/playground',
    playgroundHandler(
      config: PlaygroundConfig(
        endpoint: endpoint,
        subscriptionEndpoint: subscriptionEndpoint,
      ),
    ),
  );
  // Simple endpoint to download the GraphQLSchema as a SDL file.
  // $ curl http://localhost:8080/graphql-schema > schema.graphql
  const downloadSchemaOnOpen = true;
  const schemaFilename = 'schema.graphql';
  app.get('/graphql-schema', (Request request) {
    return Response.ok(
      schema.schemaStr,
      headers: {
        'content-type': 'text/plain',
        'content-disposition': downloadSchemaOnOpen
            ? 'attachment; filename="$schemaFilename"'
            : 'inline',
      },
    );
  });

Once you set up all the handlers, you can start the server adding middlewares if necessary. In this example, we will use the etag and cors middlewares from package:leto_shelf. You can read more about them in the package's README.

  // Set up other shelf handlers such as static files

  // Start the server
  final server = await shelf_io.serve(
    const Pipeline()
        // Configure middlewares
        .addMiddleware(customLog(log: (msg) {
          // TODO:
          if (!msg.contains('IntrospectionQuery')) {
            print(msg);
          }
        }))
        .addMiddleware(cors())
        .addMiddleware(etag())
        .addMiddleware(jsonParse())
        // Add Router handler
        .addHandler(app),
    '0.0.0.0',
    port,
  );
  print(
    'GraphQL Endpoint at $endpoint\n'
    'GraphQL Subscriptions at $subscriptionEndpoint\n'
    'GraphQL Playground UI at http://localhost:$port/playground',
  );

  return server;
}

With the runServer function finished, we can now create a main function that executes it and servers the implemented logic in a GraphQL server. This function can also be used for test as shown in the testServer function from the next section.

Future<void> main() async {
  final server = await runServer();
  final url = Uri.parse('http://${server.address.host}:${server.port}/graphql');
  await testServer(url);
}

Test the server

You can test the server programmatically by sending HTTP requests to the server. You could also test the GraphQL executor directly using the GraphQL.parseAndExecute function without running the shelf server.

/// For a complete GraphQL client you probably want to use
/// Ferry (https://github.com/gql-dart/ferry)
/// Artemis (https://github.com/comigor/artemis)
/// or raw GQL Links (https://github.com/gql-dart/gql/tree/master/links)
Future<void> testServer(Uri url) async {
  final before = DateTime.now();
  const newState = 'NewState';
  // POST request which sets the state
  final response = await http.post(
    url,
    body: jsonEncode({
      'query':
          r'mutation setState ($state: String!) { setState(newState: $state) }',
      'variables': {'state': newState}
    }),
    headers: {'content-type': 'application/json'},
  );
  assert(response.statusCode == 200);
  final body = jsonDecode(response.body) as Map<String, Object?>;
  final data = body['data']! as Map<String, Object?>;
  assert(data['setState'] == true);

  // Also works with GET
  final responseGet = await http.get(url.replace(
    queryParameters: <String, String>{
      'query': '{ getState { state createdAt } }'
    },
  ));
  assert(responseGet.statusCode == 200);
  final bodyGet = jsonDecode(responseGet.body) as Map<String, Object?>;
  final dataGet = bodyGet['data']! as Map<String, dynamic>;
  assert(dataGet['getState']['state'] == newState);
  final createdAt = DateTime.parse(dataGet['getState']['createdAt'] as String);
  assert(createdAt.isAfter(before));
  assert(createdAt.isBefore(DateTime.now()));

  // To test subscriptions you can open the playground web UI at /playground
  // or programatically using https://github.com/gql-dart/gql/tree/master/links/gql_websocket_link,
  // an example can be found in test/mutation_and_subscription_test.dart
}

Test and explore the server manually in the explorer interface "http://localhost:8080/playground". It supports subscriptions, subscribe in one tab and send a mutation request in another to test it. There are other UI explorers that you can set up (for example, GraphiQL and Altair), for more information Web UI explorers section.

We also set up a "http://localhost:8080/graphql-schema" endpoint which returns the GraphQL schema String in the schema definition language, this could be useful for other tools such as client side code generators.

Examples

Beside the tests from each package, you can find some usage example in the following directories:

Code Generator example

An example with multiple ways of creating a GraphQLSchema with different types and resolvers from code generation can be found in https://github.com/juancastillo0/leto/tree/main/leto_generator/example.

Fullstack Dart Chat

A fullstack Dart example with Flutter client and Leto/Shelf server can be found in https://github.com/juancastillo0/leto/tree/main/chat_example. The server is in the server folder.

  • Sqlite3 and Postgres database integrations
  • Subscriptions
  • Authentication/Authorization
  • Sessions
  • Tests
  • File uploads
  • Client/Server GraphQL extensions integration
  • Docker

Chat functionalities

  • Send/receive/delete messages in realtime
    • File uploads
    • Link metadata
    • Reply to other messages
  • Client cache through Ferry and Hive
  • Create chat rooms, add/remove users and share authorized invite links
  • View complete usage history with events for the most important mutations
  • View all user sessions

Server example

A Leto/Shelf server example with multiple models, code generation, some utilities and tests can be found in https://github.com/juancastillo0/leto/tree/main/leto_shelf/example

Packages

This repository is a monorepo with the following packages

Pub Source Description
version package:leto GraphQL server (executor) implementation, GraphQL extensions and DataLoader
version package:leto_schema Define GraphQL executable schemas, validate GraphQL documents and multiple utilities
version package:leto_generator Generate GraphQL schemas, types and fields from Dart code annotations
version package:leto_shelf GraphQL web server bindings and utilities for shelf
version package:leto_links Client gql links, support for GraphQL extensions defined in package:leto

Web integrations

Server integrations

Shelf

Using the shelf package.

Web UI Explorers

These web pages will allow you to explore your GraphQL Schema, view all the types and fields, read each element's documentation, and execute requests against a GraphQL server.

Usually exposed as static HTML in your deployed server. Each has multiple configurations for determining the default tabs, queries and variables, the GraphQL HTTP and WebSocket (subscription) endpoints, the UI's theme and more.

All of the static HTML files and configurations can be found in the graphql_ui folder.

GraphiQL

Documentation. Use graphiqlHandler. The classic GraphQL explorer

Playground

Documentation. Use playgroundHandler. Support for multiple tabs, subscriptions.

Altair

Documentation. Use altairHandler. Support for file Upload, multiple tabs, subscriptions, plugins.

Clients

For a complete GraphQL client you probably want to use:

Documentation

The following sections introduce most of the concepts and small examples for building GraphQL executable schemas and servers with Leto. Please, if there is something that may be missing from the documentation or you have any question you can make an issue, that would help us a lot.

GraphQL Schema Types

GraphQL Specification

The GraphQL schema type systems provides

Scalars

GraphQL Specification

Standard GraphQLScalarTypes: String, Int, Float, Boolean and ID types are already implemented and provided by Leto.

Other scalar types are also provided:

  • Json: a raw JSON value with no type schema. Could be a Map<String, Json>, List, num, String, bool or null.
  • Uri: Dart's Uri class, serialized using Uri.toString and deserialized with Uri.parse
  • Date: Uses the DateTime Dart class. Serialized as an ISO-8601 String and de-serialized with DateTime.parse.
  • Timestamp: same as Date, but serialized as an UNIX timestamp.
  • Time: // TODO:
  • Duration: // TODO:
  • Upload: a file upload. The multipart request spec

To provide your own or support types from other packages you can use Custom Scalars.

Enums

GraphQL Specification

Enums are text values which are restricted to a set of predefined variants. Their behavior is similar to scalars and they don't have a nested fields.

They require a unique name and a set of entries mapping their string representation to the Dart value obtained after parsing.

"""The error reason on a failed sign up attempt"""
enum SignUpError {
    usernameTooShort,
    usernameNotFound,
    wrongPassword,
    passwordTooSimple,
}
import 'package:leto/leto.dart';

final signUpErrorGraphQLType = enumTypeFromStrings(
'SignUpError', [
    'usernameTooShort',
    'usernameNotFound',
    'wrongPassword',
    'passwordTooSimple',
  ],
  description: 'The error reason on a failed sign up attempt',
);


// Or with code generation

/// The error reason on a failed sign up attempt
@GraphQLClass()
enum SignUpError {
    usernameTooShort,
    usernameNotFound,
    wrongPassword,
    passwordTooSimple,
}

Objects

GraphQL Specification

final type = objectType(
    'ObjectTypeName',
    fields: [],
);
  • With code generation
@GraphQLClass()
@JsonSerializable()
class Model {
    final String stringField;
    final int intField;
    final List<Model>? optionalModels;

    const Model({
        required this.stringField,
        required this.intField,
        required this.optionalModels,
    });
}

@Query
Future<Model> getModel(Ctx ctx) {

}

This would generate graphql_api.schema.dart


Interfaces

GraphQL Specification

  • inheritFrom

The inheritFrom function in GraphQLObjectType receives an Interface and assigns it's argument as
a super type, now the Object will implement the Interface passed as parameter.

Inputs and Input Objects

GraphQL Specification

Scalars and Enums can be passed as input to resolvers. Wrapper types such as List and NonNull types of Scalars and Enums, also can be passed, however for more complex Objects with nested fields you will need to use GraphQLInputObjectType. Similar GraphQLObjectType, a GraphQLInputObjectType can have fields.

// TODO: customDeserialize with SerdeCtx deserializers

final inputModel = GraphQLInputObjectType(
    'ModelInput',
    description: '',
    inputs: [

    ],
);

Field inputs (or Arguments) can be used in multiple places:

  • GraphQLObjectType.fields.inputs: Inputs in field resolvers

  • GraphQLInputObjectType.fields: Fields in Input Objects

  • GraphQLDirective.inputs: Inputs in directives

Not all types can be input types, in particular, object types and union types can't be input types nor part of a GraphQLInputObjectType.

static bool isInputType(GraphQLType type) {
  return type.when(
    enum_: (type) => true,
    scalar: (type) => true,
    input: (type) => true,
    object: (type) => false,
    union: (type) => false,
    list: (type) => isInputType(type.ofType),
    nonNullable: (type) => isInputType(type.ofType),
  );
}

Example

input ComplexInput {
    value: String!
}

# The fields:
(
    """The amount"""
    @deprecated
    amount: Int = 2
    names: [String!]
    complex: ComplexInput!
)
@GraphQLInput()
class ComplexInput {
    const ComplexInput({required this.value});
    /// The value
    final String value;

    factory ComplexInput.fromJson(Map<String, Object?> json) =>
        ComplexInput(
            value: json['value']! as String,
        );
}

final fields = [
    GraphQLFieldInput(
        'amount',
        graphQLInt,
        defaultValue: 2,
        description: 'The amount',
        // an empty String will use the default deprecation reason
        deprecationReason: '',
    ),
    GraphQLFieldInput(
        'names',
        listOf(graphQLString.nonNull()),
    ),
    GraphQLFieldInput(
        'complex',
        compleInputGraphQLInputType.nonNull(),
    ),
];

// can be used in:
// - `GraphQLObjectType.fields.inputs`
// - `GraphQLInputObjectType.fields`
// - 'GraphQLDirective.inputs'

final object = GraphQLObjectType(
    'ObjectName',
    fields: [
        stringGraphQLType.field(
            'someField',
            inputs: fields,
            resolve: (_, Ctx ctx) {
                final Map<String, Object?> args = ctx.args;
                assert(args.containKey('complex'));
                assert(args['names'] is List<String>?);
                assert(args['amount'] is int?);
                return '';
            }
        )
    ]
);

final objectInput = GraphQLInputObjectType(
    'InputObjectName',
    fields: fields,
    // ...
);

final directive = GraphQLDirective(
    name: 'DirectiveName',
    inputs: fields,
    // ...
);

InputObject.fromJson

For code generation, each class annotated as GraphQLInput should have a factory constructor or static method name fromJson in its class definition. This will be used as the method for deserializing instances of this class.

Unions

GraphQL Specification

Similar to enums, Unions are restricted to a set of predefined variants, however the possible types are always the more complex GraphQLObjectType.

Per the GraphQL spec, Unions can't be (or be part of) Input types and their possible types is a non empty collection of unique GraphQLObjectType.

To have the following GraphQL type definitions:

union ModelEvent = ModelAdded | ModelRemoved

type ModelRemoved {
  "The removed model id"
  modelId: ID!
}

type ModelAdded {
  model: Model!
}

type Model {
  id: ID!
}

You could provide this definitions:

import 'package:leto_schema/leto_schema.dart';

final model = objectType(
    'Model',
    fields: [
        graphQLIdType.nonNull().field('id'),
    ],
);
final modelAddedGraphQLType = objectType(
    'ModelAdded',
    fields: [model.nonNull().field('model')],
);
final modelRemovedGraphQLType = objectType(
    'ModelRemoved',
    fields: [graphQLIdType.nonNull().field('modelId')],
);

final union = GraphQLUnionType(
    // name
    'ModelEvent',
    // possibleTypes
    [
       modelAddedGraphQLType,
       modelRemovedGraphQLType,
    ],
);
  • extractInner

When the members of the union type are not

Freezed Unions

With code generation, Unions with freezed also work without trouble.

import 'package:leto_schema/leto_schema.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

@GraphQLClass()
class Model {
  final String id;

  const Model(this.id);
}

@GraphQLClass()
@freezed
class ModelEvent with _$ModelEvent {
  const factory ModelEvent.added(Model model) = ModelAdded;
  const factory ModelEvent.removed(
    @GraphQLDocumentation(type: 'graphQLIdType', description: 'The removed model id')
    String modelId,
    // you can also provide a private class
  ) = _ModelRemoved;
}
GraphQLAttachments unionNoFreezedAttachments() => const [ElementComplexity(50)];

@AttachFn(unionNoFreezedAttachments)
@GraphQLDocumentation(
  description: '''
Description from annotation.

Union generated from raw Dart classes
''',
)
@GraphQLUnion(name: 'UnionNoFreezedRenamed')
class UnionNoFreezed {
  const factory UnionNoFreezed.a(String value) = UnionNoFreezedA.named;
  const factory UnionNoFreezed.b(int value) = UnionNoFreezedB;
}

@GraphQLClass()
class UnionNoFreezedA implements UnionNoFreezed {
  final String value;

  const UnionNoFreezedA.named(this.value);
}

@GraphQLClass()
class UnionNoFreezedB implements UnionNoFreezed {
  final int value;

  const UnionNoFreezedB(this.value);
}

@Query()
List<UnionNoFreezed> getUnionNoFrezzed() {
  return const [UnionNoFreezed.a('value'), UnionNoFreezed.b(12)];
}

Wrapping Types

Wrapping types allow to modify the behavior of the inner (wrapped) type. The inner types can be of any GraphQLType and wrapping types can be Output or Input Types if the wrapped type is an Output or Input type. GraphQL has two wrapping types, GraphQLNonNullType and GraphQLListType.

Non-Nullable

GraphQL Specification

GraphQLNonNullType allows you to represent a non-nullable or required value. By default, all GraphQL Types are nullable or optional, if you want to represent a required input or specify that a given output is always present (non-null), you want to use the GraphQLNonNullType wrapping type.

In GraphQL this is represented using the ! exclamation mark after a given type expression. In Dart you can use the nonNull() method present in each GraphQLType, which will return a non-nullable GraphQLNonNullType with it's inner type, the type from which nonNull was called. For example, graphQLString.nonNull() will be a String! in GraphQL.

Lists

GraphQL Specification

GraphQLListType allows you to represent a collection of values.

This values can be of any GraphQLType and List types can be Output or Input Types if the Wrapped type is an Output or Input type. For example, a List of Union types is an Output type while a List of Strings (scalar types) can be an Output or Input type. You can use the <type>.list() method present in each GraphQLType or the listOf(<type>) global utility function to create a GraphQLListType. For example, graphQLString.list() will be a [String] in GraphQL.

Example

In GraphQL, you can represent it like this:

type Model {
  listField(listInput: [String]!): [InterfaceModel!]
}

interface InterfaceModel {
  name: String
}

Using Dart:

import 'package:leto_schema/leto_schema.dart';

abstract class InterfaceModel {
  String get name;
}

class Model {
  List<InterfaceModel>? list(List<String?> listInput) {
    throw Unimplemented();
  }
}

final interfaceModel = objectType<InterfaceModel>(
  'InterfaceModel',
  fields: [
    graphQLString.field(
      'name',
      resolve: (InterfaceModel obj, Ctx ctx) => obj.name,
    )
  ],
  isInterface: true,
);

final model = objectType<Model>(
  'Model',
  fields: [
    interfaceModel.nonNull().list().field(
      'listField',
      inputs: [
        listOf(graphQLString).nonNull().inputField('listInput'),
      ],
      resolve: (Model obj, Ctx ctx) => 
        obj.listField(ctx.args['listInput'] as List<String?>) 
    )
  ]
);

With code generation, you just annotate the different classes with @GraphQLClass() (or the expected annotation) and the fields and models containing Dart Lists or non-nullable types will be generated using GraphQLListType or GraphQLNonNullType as required.

import 'package:leto_schema/leto_schema.dart';

@GraphQLClass()
abstract class InterfaceModel {
  String get name;
}

@GraphQLClass()
class Model {
  List<InterfaceModel>? list(List<String?> listInput) {
    throw Unimplemented();
  }
}

Abstract Types

Abstract types like Interfaces and Unions, require type resolution of its variants on execution. For that, we provide a couple of tools explained in the following sections. You can read the code that executes the following logic in package:leto's GraphQL.resolveAbstractType method.

resolveType

A parameter of Interface and Union types is a function with the signature: String Function(Object result, T abstractType, ResolveObjectCtx ctx). Given a resolved result, the abstract type itself and the ObjectCtx, return the name of the type associated with the result value.

Generics

We compare the resolved result's Dart type with the possible types generic type parameter, if there is only one match (withing the possible types), that will be the resolved type. This happens very often, specially with code generation or when providing a distinct class for each GraphQLObjectType.

This can't be used with Union types which are wrappers over the inner types (like Result<V, E> or Either<L, R>), since generic type of the possible types (V and E) will not match the wrapper type (Result). For this cases you will need to provide a resolveType and extractInner callbacks. With freezed-like unions you don't have to do that since the variants extend the union type.

isTypeOf

If any of the previous fail, you can provide a isTypeOf callback for objects, which determine whether a given value is an instance of that GraphQLObjectType.

__typename

If the resolved result is a Map and contains a key "__typename", we will use it to resolve the type by comparing it with possible types names. If there is a match, we use the matched type in the next steps of execution.

Serialize and validate

Advanced Types

Provided Types

Cyclic Types

Types which use themselves in their definition have to reuse previously created instances. The type's field lists are mutable, which allow you to instantiate the type and then modify the fields of the type. For example, an User with friends:

class User {
    const User(this.friends);
    final List<User> friends;
}
GraphQLObjectType<User>? _type;
GraphQLObjectType<User> get userGraphQLType {
    if (_type != null) return _type; // return a previous instance
    final type = objectType<User>(
        'User',
        // leave fields empty (or don't pass them)
        fields: [],
    );
    _type = type; // set the cached value
    type.fields.addAll([ // add the fields
        listOf(userGraphQLType.nonNull()).nonNull().field(
            'friends',
            resolve: (obj, _) => obj.friends,
        ),
    ]);
    return type;
}

Code generation already does it, so you don't have to worry about it when using it.

Custom Scalars

You can extend the GraphQLScalarType or create an instance directly with GraphQLScalarTypeValue. For example, to support the Decimal type from https://github.com/a14n/dart-decimal you can use the following code:

import 'package:decimal/decimal.dart';
import 'package:leto_schema/leto_schema.dart';

final decimalGraphQLType = GraphQLScalarTypeValue<Decimal, String>(
  name: 'Decimal',
  deserialize: (SerdeCtx _, String serialized) => Decimal.parse(serialized),
  serialize: (Decimal value) => value.toString(),
  validate: (String key, Object? input) => (input is num || input is String) &&
          Decimal.tryParse(input.toString()) != null
      ? ValidationResult.ok(input.toString())
      : ValidationResult.failure(
          ['Expected $key to be a number or a numeric String.'],
        ),
  description: 'A number that allows computation without losing precision.',
  specifiedByURL: null,
);

For code generation you need to provide customTypes in the build.yaml file of you project:

target:
  default:
    builders:
      leto_generator:
        options:
          customTypes:
            - name: "Decimal"
            import: "package:<your_package_name>/<path_to_implementation>.dart"
            getter: "decimalGraphQLType"

Generic Types

Work in progress

class ErrC<T> {
  final String? message;
  final T value;

  const ErrC(this.value, [this.message]);
}

GraphQLObjectType<ErrC<T?>> errCGraphQlType<T extends Object>(
  GraphQLType<T, Object> tGraphQlType, {
      String? name,
  }
) {
  return objectType(
      name ?? 'ErrC${tGraphQlType is GraphQLTypeWrapper ? (tGraphQlType as GraphQLTypeWrapper).ofType : tGraphQlType}',
      isInterface: false,
      interfaces: [],
      description: null,
    fields: [
      field('message', graphQLString,
          resolve: (obj, ctx) => obj.message,),
      field('value', tGraphQlType,
          resolve: (obj, ctx) => obj.value,)
    ],
  );
}

  • With code generation (derialization with Generic Input types is not yet supported ISSUE // TODO:)
import 'package:leto/leto.dart';
part 'errc.g.dart';

@GraphQLClass()
class ErrC<T> {
  final String? message;
  final T value;

  const ErrC(this.value, [this.message]);
}

Which generates in 'errc.g.dart':

Map<String, GraphQLObjectType<ErrC>> _errCGraphQlType = {};

/// Auto-generated from [ErrC].
GraphQLObjectType<ErrC<T>> errCGraphQlType<T extends Object>(
  GraphQLType<T, Object> tGraphQlType,
) {
  final __name =
      'ErrC${tGraphQlType is GraphQLTypeWrapper ? (tGraphQlType as GraphQLTypeWrapper).ofType : tGraphQlType}';
  if (_errCGraphQlType[__name] != null)
    return _errCGraphQlType[__name]! as GraphQLObjectType<ErrC<T>>;

  final __errCGraphQlType = objectType<ErrC<T>>(
      'ErrC${tGraphQlType is GraphQLTypeWrapper ? (tGraphQlType as GraphQLTypeWrapper).ofType : tGraphQlType}',
      isInterface: false,
      interfaces: [],
      description: null);
  _errCGraphQlType[__name] = __errCGraphQlType;
  __errCGraphQlType.fields.addAll(
    [
      field('message', graphQLString,
          resolve: (obj, ctx) => obj.message,
          inputs: [],
          description: null,
          deprecationReason: null),
      field('value', tGraphQlType.nonNull(),
          resolve: (obj, ctx) => obj.value,
          inputs: [],
          description: null,
          deprecationReason: null)
    ],
  );

  return __errCGraphQlType;
}

Resolvers

GraphQL resolvers execute the logic for each field and return the expected value typed according to the schema. In Dart this are functions that receive the parent's object value and the field's Ctx, and return the execution result. Simple fields may only return a property of the parent object value. However, there may also be complex resolvers, such as mutations, that validate the input data and create rows in a database, or queries that retrieve multiple rows according to complex authorization logic.

Queries and Mutations

Each field (GraphQLObjectField) in an object type (GraphQLObjectType) contains a resolve parameter, this will be used to resolve all fields. The first argument to resolve with be the parent object, if this field is in the root Query or Mutation Object, the value will be the the root value passed as an argument to GraphQL.parseAndExecute and a SubscriptionEvent if this is a subscription field (more in the subscription section). The second argument will be the field's Ctx, with it you can access defined Refs with Ref.get(ctx) and view more information about the resolved field or GraphQL request. When using package:leto_shelf, you can access the HTTP request and modify the HTTP response, more information in the package's README.

type Query {
  someField: String
}

type CustomMutation {
  updateSomething(arg1: Float): Date
}

"""An ISO-8601 Date."""
scalar Date

type schema {
  query: Query
  mutation: CustomMutation
}

In Dart:

final query = objectType(
  'Query',
  fields: [
    graphQLString.field(
      'someField',
      resolve: (Object? rootObject, Ctx ctx) => 'someFieldOutput',
    ),
  ],
);

final customMutation = objectType(
  'CustomMutation',
  fields: [
    graphQLDate.field(
      'updateSomething',
      inputs: [
        graphQLFloat.inputField('arg1')
      ],
      resolve: (Object? rootObject, Ctx ctx) {
        final arg1 = ctx.args['arg1'] as double?;
        return DateTime.now();
      },
    ),
  ],
);

final schema = GraphQLSchema(
  queryType: query,
  mutation: customMutation,
);

When using package:leto_shelf, POST requests can be used for Queries or Mutations. However, GET requests can only be used for Queries, if a Mutation operation is sent using a GET request, the server will return a 405 status code (MethodNotAllowed) following the GraphQL over HTTP specification.

Subscriptions

Each field (GraphQLObjectField) in an object type (GraphQLObjectType) contains a subscribe parameter that receives the root value and a Ctx, and returns a Stream of values of the field's type Stream<T> Function(Ctx<P> ctx, P parent). The Stream of values will be returned in the data field of the GraphQLResult returned in execution.

You can only

If using a WebSocket server, the client should support either graphql-transport-ws or graphql-ws sub-protocols.

final apiSchema = GraphQLSchema(
    queryType: objectType('Query'),
    subscriptionType: objectType(
        'Subcription',
        fields: [
            graphQLInt.nonNull().fields(
                'secondsSinceSubcription',
                subscribe: (Ctx ctx, Object rootValue) {
                    return Stream.periodic(const Duration(seconds: 1), (secs) {
                        return secs;
                    });
                }
            ),
        ]
    ),
);

Future<void> main() async {
    final GraphQLResult result = await GraphQL(apiSchema).parseAndExecute(
        'subscription { secondsSinceSubcription }',
    );

    assert(result.isSubscription);
    final Stream<GraphQLResult> stream = result.subscriptionStream!;
    stream.listen((event) {
        final data = event.data as Map<String, Object?>;
        assert(data['secondsSinceSubcription'] is int);

        print(data['secondsSinceSubcription']);
    });
}

The resolve callback in a subscription field will always receive a SubscriptionEvent as it's parent. From that you can access the event value with SubscriptionEvent.value which will be the emitted by the Stream returned in the subscribe callback. The error handling in each callback is different, if an error is thrown in the subscribe callback, the Stream will end with an error. But if you throw an error in the resolve callback it will continue sending events, just the event resolved with a thrown Object will have GraphQLErrors as a result of processing the thrown Object (More information in Error Handling).

For usage in a web server you can use any of the web server integrations which support WebSocket subscriptions (For example, leto_shelf).

Examples

For a complete subscriptions example with events from a database please see the chat_example, in particular the events directory.

Request Contexts

All Ctxs implement GlobalsHolder, so that then can be used to retrieve values from the scoped map, more in ScopedMap.

Ctx

Source Code

A unique context for each field resolver

  • args: the arguments passed as inputs to this field
  • object: the parent Object's value, same as the first parameter of resolve.
  • objectCtx: the parent Object's execution context (ObjectExecutionCtx)
  • field: The GraphQLObjectField being resolved
  • path: The path to this field
  • executionCtx: The request's execution context (ExecutionCtx)
  • lookahead: A function for retrieving nested selected fields. More in the LookAhead section

ObjectExecutionCtx

This is the context associated with an object execution, can be retrieved through Ctx.objectCtx. There will be as many instances as there are objects to execute in the request. Contains the value of the object, the field selections and the path in the GraphQL request to this object.

ExecutionCtx

This is the context associated with the execution phase of the request, created after the validation phase. Contains validated and coerced (parsed) input values and the specific validated operation within the request's document to execute. It has an errors list with the encountered errors during execution. Can be retrieved with ObjectExecutionCtx.executionCtx.

RequestCtx

This is the base context associated with the request, contains the raw information about the GraphQL document, the raw (not validated nor parsed) input values, input extensions, the schema, root value and the scoped map for this request. Can be retrieved with ExecutionCtx.requestCtx.

Validation

Schema Validation

GraphQL Specification

Implements the "Type Validation" sub-sections of the specification's "Type System" section.

Guaranties that the GraphQLSchema instance is valid, verifies the Type System validations in the specification. For example, an Object field's type can only be an Output Type or an Union should have at least one possible type and all of them have to be Object types.

This will be executed before stating a GraphQL server. Leto implements all of the Specification's schema validation. The code for all rules can be found in the validate_schema.dart file in package:leto_schema.

Document Validation

GraphQL Specification

This will be executed before executing any request. Leto implements all of the Specification's document validation. The code for all rules can be found in the validate folder in package:leto_schema.

You can add custom validation rules to a server with the GraphQL.customValidationRules parameter, they will be added on top of the specifiedValidationRules. One example of a custom validation rule is the Query Complexity validation.

Query Complexity

Tests

This document validation rule allows you to restrict the complexity of a GraphQL request.

The provided queryComplexityRuleBuilder returns a ValidationRule that reports errors when the maxComplexity or maxDepth configuration parameters are reached.

  • maxComplexity

Specifies the maximum complexity for a given operation. The complexity is measured based on the selected fields and should be. If this complexity is surpassed (is greater) a validation error will be reported.

  • maxDepth

Specifies the maximum depth for a given operation. The depth is defined as the number of objects (including the root operation object) that have to be traversed to arrive to a given field. If this depth is surpassed (is greater) a validation error will be reported.

The complexity for each fieldNode is given by:

complexity = fieldComplexity + (childrenComplexity + fieldTypeComplexity) * complexityMultiplier

Where fieldComplexity is the ElementComplexity in GraphQLObjectField.attachments or defaultFieldComplexity if there aren't any.

childrenComplexity is:

  • scalar or enum (leaf types): 0
  • object or interface: sum(objectFieldsComplexities)
  • union: max(possibleTypesComplexities)

fieldTypeComplexity will be taken as the ElementComplexity from GraphQLNamedType.attachments or 0 if there aren't any.

If the fieldType is a GraphQLListType, complexityMultiplier will be the provided listComplexityMultiplier, otherwise 1.

Skip validation with Persisted Queries

Using the PersistedQueriesExtensions you can set the skipValidation parameter so that the validation is skipped for already cached (and validated) documents.

Input Validation

daw

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

da

Error Handling

daw

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

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:

Solving the N+1 problem

When fetching nested fields, a specific resolvers could be executed multiple times for each request since the parent object will execute it for all its children. This may pose a problem when the resolver has to do non-trivial work for each execution. For example, retrieving a row from a database. To solve this problem, Leto provides you with two tools: LookAhead and DataLoader.

LookAhead (Eager loading)

You can mitigate the N+1 problem by fetching all the necessary information from the parent's resolver so that when the nested fields are executed they just return the previously fetch items. This would prevent all SQL queries for nested fields since the parent resolver has all the information about the selected nested fields and can use this to execute a request that fetches the necessary columns or joins.

@GraphQLClass()
class Model {
    final String id;
    final String name;
    final NestedModel? nested;

    const Model(this.id, this.name, this.nested);
}

@GraphQLClass()
class NestedModel {
    final String id;
    final String name;

    const NestedModel(this.id, this.name);
}

final modelRepo = RefWithDefault.global(
    (GlobalsHolder scope) => ModelRepo();
);

class ModelRepo {
    List<Model> getModels({bool withNested = false}) {
        // request the database
        // if `withNested` = true, join with the `nestedModel` table
        throw Unimplemented();
    }
}

@Query()
FutureOr<List<Model>> getModels(Ctx ctx) {
    final PossibleSelections lookahead = ctx.lookahead();
    assert(!lookahead.isUnion);
    final PossibleSelectionsObject lookaheadObj = lookahead.asObject;
    final withNested = lookaheadObj.contains('nested');

    final ModelRepo repo = modelRepo.get(ctx);
    return repo.getModels(withNested: withNested);
}

With this implementation and given the following queries:

query getModelsWithNested {
  getModels {
    id
    name
    nested {
      id
      name
    }
  }
}

query getModelsBase {
  getModels {
    id
    name
  }
}

ModelRepo.getModels will receive true in the withNested param for the getModelsWithNested query since lookaheadObj.contains('nested') will be true. On the other hand, the withNested param will be false for the getModelsBase query since the "nested" field was not selected.

In this way, ModelRepo.getModels knows what nested fields it should return. It could add additional joins in a SQL query, for example.

The PossibleSelections class has the information about all the nested selected fields when the type of the field is a Composite Type (Object, Interface or Union). When it's an Union, it will provide a map from the type name Object variants to the given variant selections. The @skip and @include directives are already taken into account. You can read more about the PossibleSelections class in the source code.

DataLoader (Batching)

The code in Leto is a port of graphql/dataloader.

An easier to implement but probably less performant way of solving the N+1 problem is by using a DataLoader. It allows you to batch multiple requests and execute the complete batch in a single function call.

@GraphQLClass()
class Model {
    final String id;
    final String name;
    final int nestedId;

    const Model(this.id, this.name, this.nestedId);

    NestedModel nested(Ctx ctx) {
        return modelNestedRepo.get(ctx).getNestedModel(nestedId);
    }
}

class NestedModelRepo {

  late final dataLoader = DataLoader.unmapped<String, NestedModel>(getNestedModelsFromIds);

  Future<List<NestedModel>> getNestedModel(String id) {
    // Batch the id, eventually `dataLoader` will execute
    // `getNestedModelsFromIds` with a list of batched ids
    return dataLoader.load(id);
  }

  Future<List<NestedModel>> getNestedModelsFromIds(List<String> ids) {
      // Multiple calls to `Model.nested` will be batched and
      // all ids will be passed in the `ids` argument

      // request the database
      final List<NestedModel> models = throw Unimplemented();

      // Make a map from id to model instance
      final Map<String, NestedModel> modelsMap = models.fold(
        {}, (map, model) => map..[model.id] = model
      );
      // Return the models in the same order as the `ids` argument
      return List.of(ids.map((id) => modelsMap[id]!));
  }
}

final modelNestedRepo = RefWithDefault.global(
    (scope) => NestedModelRepo()
);


@Query()
List<Model> getModels(Ctx ctx) {
    return modelRepo.get(ctx).getModels();
}

The DataLoader has some options for configuring it. For example you can specify the maximum size of the batch (default: 2^53 or the maximum javascript integer), whether to batch requests or not (default: true) and provide a custom batch schedule function, by default it will use Future.delayed(Duration.zero, executeBatch).

You can also configure caching by providing a custom cache implementation, a custom function that maps the key passed to DataLoader.load to the cache's key or disabling caching in the DataLoader.

Combining LookAhead with DataLoader

You can use both, LookAhead and DataLoader at the same time. The keys provided to the DataLoader.load function can be anything, so you could send the PossibleSelection information, for example.

Extensions

Extensions implement additional functionalities to the server's parsing, validation and execution. For example, extensions for tracing (GraphQLTracingExtension), logging (GraphQLLoggingExtension), error handling or caching (GraphQLPersistedQueries and GraphQLCacheExtension). All extension implementations can be found in the extensions folder in package:leto.

Persisted Queries

Save network bandwidth by storing GraphQL documents on the server and not requiring the Client to send the full document String on each request.

More information: https://www.apollographql.com/docs/apollo-server/performance/apq/

Source code

Apollo Tracing

Trace the parsing, validation and execution of your GraphQL server to monitor execution times of all GraphQL requests.

More information: https://github.com/apollographql/apollo-tracing

Source code

Response Cache

Utility for caching responses in your GraphQL server and client.

Client GQL Link implementation in: // TODO:

  • Hash: Similar to HTTP If-None-Match and Etag headers. Computes a hash of the payload (sha1 by default) and returns it to the Client when requested. If the Client makes a request with a hash (computed locally or saved from a previous server response), the extension compares the hash and only returns the full body when the hash do not match. If the hash match, the client already has the last version of the payload.

  • MaxAge: If passed a Cache object, it will save the responses and compare the saved date with the current date, if the maxAge para is greater than the difference, it returns the cached value without executing the field's resolver.

  • UpdatedAt: Similar to HTTP If-Modified-Since and Last-Modified headers.

// TODO: retrive hash, updatedAt and maxAge in resolvers.

Source code

Logging Extension

The logging extension allows you monitor requests and responses executed by your server.

Provides some utilities for printing and retrieving information from execution, logging errors and provides a default GraphQLLog class that contains aggregated information about the request.

Source code

Map Error Extension

Simple extension for mapping an error catched on resolver execution.

With a function that receives the thrown error and some context as parameter and returns a GraphQLException?, this extension will override the error and pass it to the executor, which will eventually return it to the user as an error in the response's errors list.

Source code

Custom Extensions

To create a custom extension you can extend GraphQLExtension and override the necessary functions, all of which are executed throughout a request's parsing, validation and execution.

To save state scoped to a single request you can use the ScopedMap.setScoped(key, value) and retrieve the state in a different method with final value = ScopedMap.get(key);. Where the ScopedMap can be accessed with ctx.globals.

All extensions are implemented in this way, so you can look at the source code for some examples.

Directives

For more information: GraphQL specification

GraphQLDirective allows you to provide more information about different elements of your schema and queries.

The default skip, include, deprecated and specifiedBy directives are provided. Fields in the different type system definition classes allow you to include the deprecated reason for fields or enum values, and a url of the specification for scalar types. This information will be printed when using the printSchema utility, can be retrieved in Dart through GraphQL extension for modifying the behavior of request execution or, if introspection is enabled, will be exposed by the GraphQL server.

The skip and include directives are supported during document execution following the spec. Right now, custom directives on execution can be obtained by using the parsed DocumentNode from package:gql, in the future better support could be implemented.

Provide custom directives supported by your server through the GraphQLSchema.directives field.

You can retrieve custom directives values in your GraphQL Schema definition when using the buildSchema utility, which will parse all directives and leave them accessible through the astNode Dart fields in the different GraphQL elements. Setting custom directives values through the GraphQL Schema Dart classes is a work in progress. Right now, you can add DirectiveNodes to the element's attachments if you want to print it with printSchema, however the api will probably change. See https://github.com/graphql/graphql-js/issues/1343

Attachments

This api is experimental.

All GraphQL elements in the schema can have addition custom attachments. This can be used by other libraries or extensions to change the behavior of execution. For example, for supporting custom input validations or configuring the max age for some fields in an extension that caches responses.

Utilities

Most GraphQL utilities can be found in the utilities folder in package:leto_schema.

buildSchema

Create a GraphQLSchema from a GraphQL Schema Definition (SDL) document String.

printSchema

Transform a GraphQLSchema into a String in the GraphQL Schema Definition Language (SDL).

extendSchema

Experimental. Extend a GraphQLSchema with an SDL document. This will return an extended GraphQLSchema with the additional types, fields, inputs and directives provided in the document.

introspectionQuery

Create an introspection document query for retrieving Schema information from a GraphQL server.

mergeSchemas

Experimental. Merge multiple GraphQLSchema. The output GraphQLSchema contains all the query, mutations and subscription fields from the input schemas. Nested objects are also merged.

schemaFromJson

Experimental. Build a GraphQLSchema from a JSON value, will add query, mutation, subscription and custom events on top of the provided JSON value. Will try to infer the types from the JSON structure.

Contributing

Thanks for considering making a contribution! Every issue or question helps!

This package uses melos to manage dependencies. To install it run:

pub global activate melos

Then, to link the local packages run:

melos bootstrap

If using fvm, you may need to run:

fvm flutter pub global run melos bootstrap
Comments
  • Question: Packages not published?

    Question: Packages not published?

    I feel like I'm missing something. The README install instructions say add these dependencies but it doesn't appear those packages are published to pub.dev? 😁

    dependencies:
      leto_schema: ^0.0.1
      leto: ^0.0.1
      leto_shelf: ^0.0.1
      shelf: ^1.0.0
      shelf_router: ^1.0.0
      # Not nessary for the server, just for testing it
      http: ^1.0.0
    
    dev_dependencies:
      # Only if you use code generation
      leto_generator: ^0.0.1
      build_runner: ^2.0.0
    
    opened by kmcgill88 4
  • Update object params values when variables

    Update object params values when variables

    variable.name is a NamedNode type, but the variables array keys are Strings of the name of the variable. This causes any variables to not be replaced correctly when they are used in an Object.

    Updated to use the value of the NamedNode which can be looked up in the variables Map.

    opened by warrenisarobot 2
  • Git clone on the repo fails on windows because filenames contain ':'

    Git clone on the repo fails on windows because filenames contain ':'

    I've been using the repo by adding dependency_overrides in my pubspec.yaml as suggested in this Issue https://github.com/juancastillo0/leto/issues/3.

    My primary OS is Windows 10.

    Because they contain a colon, the "Validation: ..." json files located in https://github.com/juancastillo0/leto/tree/main/leto_schema/test/validation/scenarios cause git clone to fail.

    This is because a colon is an invalid character for a Windows filename.

    opened by wellers 2
  • Query variables aren't used as values when used in an InputType

    Query variables aren't used as values when used in an InputType

    In Leto, when a query/mutation takes scalars for input variable substituion works fine. This function and query work: Dart function:

    @Query()
    //Get the first of all rewards
    String something(Ctx ctx, String bob) {
      return "bob: $bob";
    }
    

    Client query:

    mutation n($bob: String!){
      something(bob: $bob)
    }
    

    Server-side variable substitution works correctly. However, using using an input type this does not work as expected:

    Dart function:

    @GraphQLInput()
    @JsonSerializable()
    class BobInput {
      final String bob;
    
      BobInput(this.bob);
    
      factory BobInput.fromJson(Map<String, dynamic> json) =>
          _$BobInputFromJson(json);
    
      Map<String, dynamic> toJson() => _$BobInputToJson(this);
    }
    
    @Query()
    String something(Ctx ctx, BobInput bobInput) {
      return "bob: ${bobInput.bob}";
    }
    

    Query:

    query n($email: String!) {
      something(bobInput: { bob: $email })
    }
    

    This throws an exception on type coercion even when the varaible is set to a non-null value.

    PR #6 addresses this

    opened by warrenisarobot 0
  • Update graphql reservedTypeNamePrefix regex

    Update graphql reservedTypeNamePrefix regex

    Per the graph spec the only reserved Type names begin with double underscore __.

    This causes and issue when attempting to integrate Apollo Federation spec which includes the below.

    union _Entity
    
    scalar _Any
    
    type _Service {
      sdl: String!
    }
    
    
    opened by kmcgill88 0
  • Multithreading support

    Multithreading support

    This might be a feature request or a question depending on the feature's state.

    Can Leto handle multiple GraphQL requests to the endpoint in parallel? Or does the second request need to wait for the first to complete?

    Say we first requests coming and taking 5 seconds because it is computationally expensive. After 1 second, another request comes. Does the second request wait for 4 seconds before it is handled?

    If parallel processing is not possible, I would like to request this feature.

    opened by RastislavMirek 1
Owner
Juan Manuel Castillo
Juan Manuel Castillo
Utilities for loading and running WASM modules from Dart code

Provides utilities for loading and running WASM modules Built on top of the Wasmer runtime. Setup Run dart run wasm:setup to build the Wasmer runtime.

Dart 284 Dec 23, 2022
This project is the HERE SDK reference application for Flutter

HERE SDK Reference Application for Flutter The reference application for the HERE SDK for Flutter (Navigate Edition) shows how a complex and release-r

HERE Technologies 32 Dec 15, 2022
Build dart types from GraphQL schemas and queries

Artemis Build dart types from GraphQL schemas and queries Check the beta branch for the bleeding edge (and breaking) stuff. Artemis is a code generato

Igor Borges 481 Nov 27, 2022
Libraries supporting GraphQL in Dart

gql-dart/gql This is an effort to advance the Dart GraphQL ecosystem. It consists of multiple packages and libraries centered around GraphQL AST. Core

GQL Dart 240 Dec 2, 2022
A collection of helpful utilities for use in Dart projects.

w_common A collection of helpful utilities for use in Dart projects. Right now, it includes the following: Disposable interface / mixin to assist with

Workiva 10 Dec 16, 2022
Algorithm Toolbox is an Android app for C++, Python and DART algorithms. It shows the codes as well as its explanation with various examples.

AlgoKing Algorithm Toolbox is an Android app for C++, Python and DART algorithms. It shows the codes as well as its explanation with various examples.

Hash Studios 5 Sep 13, 2022
This project is a implementation of a collection of terminal server samples in Dart

Terminal Server Samples ⭐ Star me on GitHub — it helps! This project is a implementation of a collection of terminal server samples in Dart licensed u

Javier Montenegro 0 Nov 26, 2021
A collection of Flutter examples and demos.

Flutter samples A collection of open source samples that illustrate best practices for Flutter. Visual samples index The easiest way to browse through

Flutter 14.1k Jan 7, 2023
Flutter codelab examples

Flutter codelabs This repository contains the code for a variety of Flutter Codelabs. Here is a list of the codelabs represented here: Adding Google M

Flutter 1k Dec 30, 2022
An expressive, functional, and full-featured server-side framework for Dart.

A framework and collection of packages for writing http servers, built on top of the shelf package. This framework is intended to reduce the technical

Marathon 45 Jun 25, 2022
Dart HTTP server framework for building REST APIs. Includes PostgreSQL ORM and OAuth2 provider.

Aqueduct is a modern Dart HTTP server framework. The framework is composed of libraries for handling and routing HTTP requests, object-relational mapp

Stable Kernel 2.4k Jan 5, 2023
server side dart micro-framework to handle incoming http requests

Queen Palace ???? Introduction server side dart micro-framework to handle incoming http requests

Ahmed Masoud 32 Aug 30, 2022
Dart API to speak to a moderator server

Interzone is the dart API to talk with moderator, a tiny ephemeral forum basic blocks: world.dart, a world scaffold each world you can think of as one

null 8 Jan 17, 2022
A dart client for Supabase Realtime server.

realtime-dart Listens to changes in a PostgreSQL Database and via websockets. A dart client for Supabase Realtime server. Usage Creating a Socket conn

Supabase Community 76 Dec 14, 2022
Open source password generator app for Android and iOS

Parole is an open-source, free, and cross-platform service that is intended to make people secure on the internet ecosystem. Parole excels to build up

Shivam Yadav 1 Oct 19, 2021
Chopper is an http client generator using source_gen and inspired from Retrofit.

Chopper Chopper is an http client generator for Dart and Flutter using source_gen and inspired by Retrofit. Documentation Installation Please refer to

Hadrien Lejard 632 Dec 31, 2022
GetDoctor is a complete app developed in Flutter, Firebase and Blazor,.Net Core API and SQL Server

GetDoctor ?? ?? ?? GetDoctor is a complete app developed in Flutter, Firebase and Blazor,DotNet Core API and SQL Server GetDoctor is a complete packag

Sunil Vijayan 69 Dec 19, 2022
Nakama is an open-source server designed to power modern games and apps

Nakama is an open-source server designed to power modern games and apps. Features include user accounts, chat, social, matchmaker, realtime multiplayer, and much more.

Allan Nava 85 Dec 30, 2022
A complete grocery store developed with Flutter, .Net Core, Firebase, One Signal and SQL Server as backend

# Grocery-Store developed in Flutter,DotNet Core, Firebase, One-Signal, SQL-Server, Stripe, Razorpay, Paypal A complete grocery store developed with F

Sunil Vijayan 31 Jan 1, 2023