Schema Types

A GraphQL schema might look like the one below. Do not be intimidated by the number of components since we will immediately proceed to dissect the schema.

schema {
    query: RootSchemaQuery
}

type RootSchemaQuery {
    Animal: [Animal]
    Entity: [Entity]
    Food: [Food]
    Species: [Species]
    Toy: [Toy]
}

directive @filter(op_name: String!, value: [String!]) on FIELD | INLINE_FRAGMENT

directive @tag(tag_name: String!) on FIELD

directive @output(out_name: String!) on FIELD

directive @output_source on FIELD

directive @optional on FIELD

directive @recurse(depth: Int!) on FIELD

directive @fold on FIELD

scalar Date

scalar DateTime

scalar Decimal

type Animal implements Entity {
    _x_count: Int
    uuid: ID
    name: String
    alias: [String]
    color: String
    birthday: Date
    net_worth: Decimal
    in_Animal_ParentOf: [Animal]
    out_Animal_ParentOf: [Animal]
    in_Entity_Related: [Entity]
    out_Entity_Related: [Entity]
    out_Animal_OfSpecies: [Species]
    out_Animal_PlaysWith: [Toy]
}

type Food implements Entity {
    _x_count: Int
    uuid: ID
    name: String
    alias: [String]
    in_Entity_Related: [Entity]
    out_Entity_Related: [Entity]
    in_Species_Eats: [Species]
}

type Species implements Entity {
    _x_count: Int
    uuid: ID
    name: String
    alias: [String]
    in_Animal_OfSpecies: [Animal]
    in_Entity_Related: [Entity]
    out_Entity_Related: [Entity]
    in_Species_Eats: [Species]
    out_Species_Eats: [Union__Food__Species]
}

type Toy {
    _x_count: Int
    uuid: ID
    name: String
    in_Animal_PlaysWith: [Animal]
}

interface Entity {
    _x_count: Int
    uuid: ID
    name: String
    alias: [String]
    in_Entity_Related: [Entity]
    out_Entity_Related: [Entity]
}

union Union__Food__Species = Food | Species

Note

A GraphQL schema can be serialized with the print_schema function from the graphql.utils.schema_printer module.

Objects types and fields

The core components of a GraphQL schema are GraphQL object types. They conceptually represent the concrete vertex types in the underlying database. For relational databases, we think of the tables as the concrete vertex types.

Lets go over a toy example of a GraphQL object type:

type Toy {
    _x_count: Int
    name: String
    in_Animal_PlaysWith: [Animal]
}

Here are some of the details:

  • _x_count: is a meta field. Meta fields are an advanced compiler feature.
  • name is a property field that represents concrete data.
  • in_Animal_PlaysWith is a vertex field representing an inbound edge.
  • String is a built-in GraphQL scalar type.
  • [Animal] is a GraphQL list representing a list of Animal objects.

Directives

Directives are keywords that modify query execution. The compiler includes a list of directives, which we’ll talk about more in the query directives section. For now lets see how they are defined by looking at an example:

directive @output(out_name: String!) on FIELD
  • @output defines the directive name.
  • out_name: String! is a GraphQL argument. The ! indicates that it must not be null.
  • on FIELD defines where the directive can be located. According to the definition, this directive can only be located next to fields. The compiler might have additional restrictions for where a query can be located.

Scalar types

The compiler uses the built-in GraphQL scalar types as well as three custom scalar types:

  • DateTime represents timezone-naive second-accuracy timestamps.
  • Date represents day-accuracy date objects.
  • Decimal is an arbitrary-precision decimal number object useful for representing values that should never be rounded, such as currency amounts.

Operation types

GraphQL allows for three operation types query, mutation and subscription. The compiler only allows for read-only query operation types as shown in the code snippet below:

schema {
    query: RootSchemaQuery
}

A query may begin in any of the root vertex types specified by the special RootSchemaQuery object type:

type RootSchemaQuery {
    Animal: [Animal]
    Entity: [Entity]
    Food: [Food]
    Species: [Species]
    Toy: [Toy]
}

Inheritance

The compiler uses interface and union types in representing the inheritance structure of the underlying schema. Some database backends do not support inheritance, (e.g. SQL), so this feature is only supported for certain backends.

Interface types

Object types may declare that they implement an interface type, meaning that they contain all property and vertex fields that the interface declares. In many programming languages, this concept is called interface inheritance or abstract inheritance. The compiler uses interface implementation in the GraphQL schema to model the abstract inheritance in the underlying database.

interface Entity {
     _x_count: Int
     name: String
     alias: [String]
     in_Entity_Related: [Entity]
     out_Entity_Related: [Entity]
 }

 type Food implements Entity {
     _x_count: Int
     name: String
     alias: [String]
     in_Entity_Related: [Entity]
     out_Entity_Related: [Entity]
     in_Species_Eats: [Species]
 }

Querying an interface type without any type coercion returns all of the the objects implemented by the interface. For instance, the following query returns the name of all Food, Species and Animal objects.

{
   Entity {
      name @output(out_name: "entity_name")
   }
}

Union types and type_equivalence_hints

GraphQL’s type system does not allow object types to inherit other object types (i.e. it has no notion of concrete inheritance). However, to model the database schema of certain backends and to emit the right query in certain cases, the compiler needs to have a notion of the underlying concrete inheritance.

In order to work around this limitation, the GraphQL compiler uses GraphQL union types as means of listing the subclasses of an object with multiple implicit subclasses. It also takes in a type_equivalence_hints parameter to match an object type with the union type listing its subclasses.

For example, suppose Food and Species are concrete types and Food is a superclass of Species in an OrientDB schema. Then the GraphQL schema info generation function would generate a union type in the schema

union Union__Food__Species = Food | Species

as well an entry in type_equivalence_hints mapping Food to Union_Food_Species.

To query an union type, one must always type coerce to one of the encompassed object types as illustrated in the section below.

Type coercions

Type coercions are operations than can be run against interfaces and unions to create a new scope whose type is different than the type of the enclosing scope of the coercion. Type coercions are represented with GraphQL inline fragments.

Example Use

{
    Species {
        name @output(out_name: "species_name")
        out_Species_Eats {
            ... on Food {
                name @output(out_name: "food_name")
            }
        }
    }
}

Here, the out_Species_Eats vertex field is of the Union__Food__FoodOrSpecies__Species union type. To proceed with the query, the user must choose which of the types in the Union__Food__FoodOrSpecies__Species union to use. In this example, ... on Food indicates that the Food type was chosen, and any vertices at that scope that are not of type Food are filtered out and discarded.

{
    Species {
        name @output(out_name: "species_name")
        out_Entity_Related {
            ... on Species {
                name @output(out_name: "entity_name")
            }
        }
    }
}

In this query, the out_Entity_Related is of Entity type. However, the query only wants to return results where the related entity is a Species, which ... on Species ensures is the case.

Constraints and Rules

  • Must be the only selection in scope. No field may exist in the same scope as a type coercion. No scope may contain more than one type coercion.

Meta fields

Meta fields are fields that do not represent a property/column in the underlying vertex type. They are also an advanced compiler feature. Before continuing, readers should familiarize themselves with the various query directives supported by the compiler.

__typename

The compiler supports the standard GraphQL meta field __typename, which returns the runtime type of the scope where the field is found. Assuming the GraphQL schema matches the database’s schema, the runtime type will always be a subtype of (or exactly equal to) the static type of the scope determined by the GraphQL type system. Below, we provide an example query in which the runtime type is a subtype of the static type, but is not equal to it.

The __typename field is treated as a property field of type String, and supports all directives that can be applied to any other property field.

Example Use

{
    Entity {
        __typename @output(out_name: "entity_type")
        name @output(out_name: "entity_name")
    }
}

This query returns one row for each Entity vertex. The scope in which __typename appears is of static type Entity. However, Animal is a type of Entity, as are Species, Food, and others. Vertices of all subtypes of Entity will therefore be returned, and the entity_type column that outputs the __typename field will show their runtime type: Animal, Species, Food, etc.

_x_count

The _x_count meta field is a non-standard meta field defined by the GraphQL compiler that makes it possible to interact with the number of elements in a scope marked @fold. By applying directives like @output and @filter to this meta field, queries can output the number of elements captured in the @fold and filter down results to select only those with the desired fold sizes.

We use the _x_ prefix to signify that this is an extension meta field introduced by the compiler, and not part of the canonical set of GraphQL meta fields defined by the GraphQL specification. We do not use the GraphQL standard double-underscore (__) prefix for meta fields, since all names with that prefix are explicitly reserved and prohibited from being used in directives, fields, or any other artifacts.

Adding the _x_count meta field to your schema

Since the _x_count meta field is not currently part of the GraphQL standard, it has to be explicitly added to all interfaces and types in your schema. There are two ways to do this.

The preferred way to do this is to use the EXTENDED_META_FIELD_DEFINITIONS constant as a starting point for building your interfaces’ and types’ field descriptions:

from graphql import GraphQLInt, GraphQLField, GraphQLObjectType, GraphQLString
from graphql_compiler import EXTENDED_META_FIELD_DEFINITIONS
fields = EXTENDED_META_FIELD_DEFINITIONS.copy()
fields.update({
    'foo': GraphQLField(GraphQLString),
    'bar': GraphQLField(GraphQLInt),
    # etc.
})
graphql_type = GraphQLObjectType('MyType', fields)
# etc.

If you are not able to programmatically define the schema, and instead simply have a pre-made GraphQL schema object that you are able to mutate, the alternative approach is via the insert_meta_fields_into_existing_schema() helper function defined by the compiler:

# assuming that existing_schema is your GraphQL schema object
insert_meta_fields_into_existing_schema(existing_schema)
# existing_schema was mutated in-place and all custom meta-fields were added

Example Use

{
    Animal {
        name @output(out_name: "name")
        out_Animal_ParentOf @fold {
            _x_count @output(out_name: "number_of_children")
            name @output(out_name: "child_names")
        }
    }
}

This query returns one row for each Animal vertex. Each row contains its name, and the number and names of its children. While the output type of the child_names selection is a list of strings, the output type of the number_of_children selection is an integer.

{
    Animal {
        name @output(out_name: "name")
        out_Animal_ParentOf @fold {
            _x_count @filter(op_name: ">=", value: ["$min_children"])
                    @output(out_name: "number_of_children")
            name @filter(op_name: "has_substring", value: ["$substr"])
                 @output(out_name: "child_names")
        }
    }
}

Here, we’ve modified the above query to add two more filtering constraints to the returned rows:

  • child Animal vertices must contain the value of $substr as a substring in their name, and
  • Animal vertices must have at least $min_children children that satisfy the above filter.

Importantly, any filtering on _x_count is applied after any other filters and type coercions that are present in the @fold in question. This order of operations matters a lot: selecting Animal vertices with 3+ children, then filtering the children based on their names is not the same as filtering the children first, and then selecting Animal vertices that have 3+ children that matched the earlier filter.

Constraints and Rules

  • The _x_count field is only allowed to appear within a vertex field marked @fold.
  • Filtering on _x_count is always applied after any other filters and type coercions present in that @fold.
  • Filtering or outputting the value of the _x_count field must always be done at the innermost scope of the @fold. It is invalid to expand vertex fields within a @fold after filtering or outputting the value of the _x_count meta field.

How is filtering on _x_count different from @filter with has_edge_degree?

The has_edge_degree filter allows filtering based on the number of edges of a particular type. There are situations in which filtering with has_edge_degree and filtering using = on _x_count produce equivalent queries. Here is one such pair of queries:

{
    Species {
        name @output(out_name: "name")
        in_Animal_OfSpecies @filter(op_name: "has_edge_degree", value: ["$num_animals"]) {
            uuid
        }
    }
}

and

{
    Species {
        name @output(out_name: "name")
        in_Animal_OfSpecies @fold {
            _x_count @filter(op_name: "=", value: ["$num_animals"])
        }
    }
}

In both of these queries, we ask for the names of the Species vertices that have precisely $num_animals members. However, we have expressed this question in two different ways: once as a property of the Species vertex (“the degree of the in_Animal_OfSpecies is $num_animals”), and once as a property of the list of Animal vertices produced by the @fold (“the number of elements in the @fold is $num_animals”).

When we add additional filtering within the Animal vertices of the in_Animal_OfSpecies vertex field, this distinction becomes very important. Compare the following two queries:

{
    Species {
        name @output(out_name: "name")
        in_Animal_OfSpecies @filter(op_name: "has_edge_degree", value: ["$num_animals"]) {
            out_Animal_LivesIn {
                name @filter(op_name: "=", value: ["$location"])
            }
        }
    }
}

versus

{
    Species {
        name @output(out_name: "name")
        in_Animal_OfSpecies @fold {
            out_Animal_LivesIn {
                _x_count @filter(op_name: "=", value: ["$num_animals"])
                name @filter(op_name: "=", value: ["$location"])
            }
        }
    }
}

In the first, for the purposes of the has_edge_degree filtering, the location where the animals live is irrelevant: the has_edge_degree only makes sure that the Species vertex has the correct number of edges of type in_Animal_OfSpecies, and that’s it. In contrast, the second query ensures that only Species vertices that have $num_animals animals that live in the selected location are returned – the location matters since the @filter on the _x_count field applies to the number of elements in the @fold scope.