Getting Started#

This will walk you through some of Magql’s features, which are described in more detail in the rest of the documentation. Read GraphQL’s tutorial first to understand how a GraphQL schema is constructed.

A Simple Example#

The Schema is the entry point into the API, and provides query and mutation objects to register fields on. Each Field has a type and a resolver function that returns data of that type. If the field defines arguments, and they are given in the operation, they will be passed to the resolver.

import magql

schema = magql.Schema()

@schema.query.field("greet", "String!", args={
    "name": magql.Argument("String!", default="World")
})
def resolve_greet(parent, info, **kwargs):
    name = kwargs.pop("name")
    return f"Hello, {name}!"

After defining the API, call Schema.execute() to execute an operation.

>>> schema.execute("""query { greet }""").data
{'greet': 'Hello, World!'}

>>> schema.execute("""query { greet(name: "Magql") }""")
{'greet': 'Hello, Magql!'}

Integrations#

Magql provides all the parts needed to define a complex and powerful GraphQL API, but it’s still a manual process. There are extensions that integrate Magql with other libraries to make this easier.

  • Magql-SQLAlchemy generates a complete API given a SQLAlchemy declarative model class. Includes item, list (with filter, sort, page), create, update, delete, search, and check delete operations. Validates ids and unique constraints.

  • Flask-Magql serves a Magql schema with Flask. Implements the multipart file upload GraphQL extension. Provides GraphiQL and Conveyor UIs.

Operations#

After building the graph (the schema), operations are executed on it. An operation describes a traversal of the graph. Each field is a step to take on a path through the graph. The result of each resolver is either data in the output, or the parent data for the next field in the path.

GraphQL distinguishes operations as queries which access data, or mutations which change data. This distinction is only at the top-level, the fields you add to the Schema.query and Schema.mutation objects. Every field’s resolver is a function, so any field could potentially do anything when it is resolved. Using the query and muatation distinction is a convention that makes it easier to reason about the API.

Technically, the GraphQL spec says that queries can be executed in parallel, while mutations must be executed in order. And there’s a third operation, subscription, which is a query that continues to stream results. However, Magql isn’t currently implemented in a way where this matters. It’s still a useful way to think about what goes where.

Types and References#

The type of each Field can be a Scalar or Enum, or an Object with more fields, creating a graph. Each field can have arguments, which also have a type. An argument type can be Scalar or Enum, but it uses InputObject with InputField for complex data, instead of Object. This can all be a bit confusing, but here’s the outline:

Types can be referred to by their name, rather than needing to import their Python objects everywhere. As long as the schema knows about the named type, it will be applied correctly when creating the GraphQL schema. Referring to types by name is more convenient, and also allows circular and forward references.

Types can be wrapped with NonNull and List. Every type has non_null and list properties that do the same. When referring to types by name, the GraphQL syntax for non-null Type! and list [Type] can be used.

The following examples are equivalent.

Field(NonNull(List(NonNull(user_object))))
Field(user_object.non_null.list.non_null)
Field("[User!]!")

See Type References and Scalar Types for more information.

Defining Structure#

Definition starts at fields on the Schema.query and Schema.mutation objects. Other Object and InputObject types can be defined and added with Schema.add_type() so that they may be referenced by name.

import magql

schema = magql.Schema()
user_object = magql.Object("User", fields={"id": "Int!", "name": "String!"})
user_input = magql.InputObject("UserInput")
schema.add_type(user_object)

When defining an object’s fields, the values can be just the type name or object instead of a Field object, which is convenient if you don’t need further customization or will do it later. The following examples are equivalent.

Object("User", fields={"id": Field("Int!")})
Object("User", fields={"id": Int.non_null})
Object("User", fields={"id": "Int!"})

This works in places that take collections during init. After a node is defined, you can modify its attributes in place. However, you must use the correct nodes at this point, the type shortcut no longer applies.

  • The Object.fields param takes a type in place of Field. The the Object.fields attr can be modified.

  • The Field.args param takes a type in place of Argument. Then the Field.args attr can be modified.

  • The InputObject.fields param takes a type in place of InputField. Then the InputField.fields attr can be modified.

Some nodes provide decorators for a quick way to add or modify behavior:

Everything about any node can be modified after it is created, not only the attributes and decorators shown here. However, all modifications are “locked” once Schema.to_graphql() is called.

Resolvers and Validation#

Each Field has a resolver function that is called when the field is traversed during an operation. If the field’s type is an object, it returns the next object to traverse, or if it is a scalar it returns the data for output.

Resolver functions all take the same three arguments, parent, info, and **kwargs. parent is the object “above” the current field, such as a User for a username field. kwargs is a dict of arguments passed to the field, which will have been validated already.

Magql generates a resolver with three different stages. The callables in each stage can raise ValidationError to add an error to the output instead of data. Before any of the resolver system runs, GraphQL scalars will have already converted the input values to the appropriate types.

  1. Field.pre_resolver() can decorate a function used to perform checks before the validators and resolver run, such as checking authentication or audit logging access.

  2. Field.validate() is called to validate the input data. Arguments, input fields, fields, and input objects can all have validators. List types can have validators for the whole list or each item in the list. Each node has a validator() decorator to add another validator.

  3. The field’s resolver is called with the validated input arguments to get the output value. The default resolver accesses parent.field_name. Field.resolver() can decorate a new resolver callable.

If validation errors occur, an error with the message magql argument validation will be present in result.errors. Its extensions property has error messages in a structure that matches that of the argument structure.

See Resolvers and Input Validation for more information.