GraphQL

Perl 6 implementation of GraphQL


GraphQL

SYNOPSIS

use GraphQL;

class Query
{
    method hello(--> Str) { 'Hello World' }
}

my $schema = GraphQL::Schema.new(Query);

say $schema.execute('{ hello }').to-json;

DESCRIPTION

“GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.” - Facebook Inc., http://graphql.org.

The GraphQL Language is described in detail at http://graphql.org which also includes the draft specification. This module is a Perl 6 server implementation of that specification (or will be once it is complete). The intent of this documentation isn’t to fully describe GraphQL and its usage, but rather to describe that Perl implementation and how various functionality is accessible through Perl. This document will assume basic awareness of GraphQL and that standard.

OVERVIEW

GraphQL itself isn’t a database, it is the interface between the client and whatever database or other data store you use. Constructing a GraphQL server consists of describing your API Schema consisting of a data structure of data Types, and connecting to subroutines or methods for Resolution of the actual data values. The Schema is the controller or orchestrator for everything. It performs two major functions, Validation to determine if a query is valid at all, and Execution, which makes calls to arbitrary code for Resolution to determine the resulting data structure. The GraphQL language also specifies Introspection which is essentially Resolution carried out by the Schema itself to describe itself.

The synopsis above describes the simplest GraphQL server possible. It consists of a single Type or Class called Query, with a single field in it called hello of type String, with a method attached to it that returns the string Hello World.

The schema is constructed by passing the Perl 6 class into the GraphQL::Schema’s new() constructor. The example then passes in the simplest GraphQL query {hello}. Execution will call the hello() method and return the result in a GraphQL::Result structure that can then be converted into JSON with to-json() method which will return the result:

{
  "data": {
    "hello": "Hello World"
  }
}

In a typical GraphQL Web server, the query would be HTTP POSTed to an endpoint at /graphql which would call GraphQL::Schema.execute() and send the resulting JSON string back to the requester.

Each of those steps will be described in more detail below.

Schema Styles

This module currently supports three different styles for expressing GraphQL types for your GraphQL schema:

  • Manual - You can construct each type by creating and nesting various GraphQL::* objects.

For the “Hello World” example, it would look like this:

my $schema = GraphQL::Schema.new(
    GraphQL::Object.new(
        name => 'Query',
        fieldlist => GraphQL::Field.new(
            name => 'hello',
            type => $GraphQLString,
            resolver => sub { 'Hello World' }
        )
    )
);

For the “Hello World” example, it would look like this:

my $schema = GraphQL::Schema.new('type Query { hello: String }',
    resolvers => { Query => { hello => sub { 'Hello World' } } });

Note that while the schema type descriptions are provided in the GSL, the resolving functions for each field must be separately supplied in a two level hash with the names of each Object Type at the first level, and Field at the second level.

  • Direct Perl Classes - You can also simply pass in Perl 6 classes directly. A matching schema is constructed by examining the classes with the Perl language Metamodel for introspection. Given the GraphQL type restrictions, not everything you can express in Perl will result in a valid Schema, so it is important to use only the types as described below. Also restrict the names of attributes and methods to the alpha-numeric and ‘_’. (No fancy unicode names, or kebab-case names.)

For the “Hello World” example, it looks like this:

class Query
{
    method hello(--> Str) { 'Hello World' }
}

my $schema = GraphQL::Schema.new(Query);

Under the hood, the Schemas all look the same, regardless of which style you use to construct them. The later two options are just additional syntactic sugar to make things easier. You can also mix and match, making some types one way and some another and everything will still work fine.

Types

GraphQL is a strongly, staticly typed language. Every type must be defined precisely up front, and all can be checked during validation phase prior to execution.

The Perl Class hierarchy for GraphQL Types includes these:

  • GraphQL::Type (abstract, not to be used directly, only inherited

  • GraphQL::Scalar

  • GraphQL::String

  • GraphQL::Int

  • GraphQL::Float

  • GraphQL::ID

  • GraphQL::EnumValue

  • GraphQL::List

  • GraphQL::Non-Null

  • GraphQL::InputValue

  • GraphQL::Field

  • GraphQL::Interface

  • GraphQL::Object

  • GraphQL::InputObjectType

  • GraphQL::Union

  • GraphQL::Enum

  • GraphQL::Directive

role Deprecatable

GraphQL::Field and GraphQL::EnumValue are Deprecatable

They get two extra public attributes $.isDeprecated Bool, default False, and $.deprecationReason Str.

They also get the method .deprecate(Str $reason), which defaults to “No longer supported.”

In GSL, you can also deprecate with the directive @deprecate or @deprecate(reason: "something"). More on directives below.

role HasFields

GraphQL::Object and GraphQL::Interface both include a role HasFields that give them a @.fieldlist array of GraphQL::Fields, a method .field($name) to look up a field, and a method .fields(Bool :$includeDeprecated) that will return the list of fields. Meta-fields with names starting with “__” are explicitly not returned in the .fields() list, but can be requested with .field().

GraphQL::Type

This is the main GraphQL type base class. It has public attributes $.name and $.description. It isn’t intended to be used directly, it is just the base class for all the other Types.

The description field can be explicitly assigned in the creation of each GraphQL::Type.

In GSL, you can set the description field by preceding the definition of types with comments:

# Description for mytype
type mytype {
  # Description for myfield
  myfield: Str
}

In Perl, the description field is set from the Meto-Object Protocol $obj.WHY method which by default will be set automatically with Pod declarations. e.g.

#| Description for mytype
class mytype {
  #| Description for myfield
  has Str $.myfield
}

GraphQL::Scalar is GraphQL::Type

Serves as the base class for scalar, leaf types. It adds the method .kind() = ‘SCALAR’;

There are several core GraphQL scalar types that map to Perl basic scalar types:

GraphQL Type Perl Type Class Perl Object Instance Perl Type
String GraphQL::String $GraphQLString Str
Int GraphQL::Int $GraphQLInt Int
Float GraphQL::Float $GraphQLFloat Num
Boolean GraphQL::Boolean $GraphQLBoolean Bool
ID GraphQL::ID $GraphQLID ID (subset of Cool)

The Perl Object Instances are just short hand pre-created objects that can be used since those types are needed so frequently.

For example, GraphQL::String.new creates a String type, but you can just use $GraphQLString which is already made.

You can create your own additional scalar types as needed:

my $URL = GraphQL::Scalar.new(name => 'URL');

or in GSL:

scalar URL

GraphQL::String is GraphQL::Scalar

Core String type, maps to Perl type Str.

You can create your own:

my $String = GraphQL::String.new;

or just use $GraphQLString.

GraphQL::Int is GraphQL::Scalar

Core Int type, maps to Perl type Int.

You can create your own:

my $Int = GraphQL::Int.new;

or just use $GraphQLInt.

GraphQL::Float is GraphQL::Scalar

Core Float type, maps to Perl type Num.

You can create your own:

my $Float = GraphQL::Float.new;

or just use $GraphQLFloat.

GraphQL::Boolean is GraphQL::Scalar

Core Boolean type, maps to Perl type Bool.

You can create your own:

my $Boolean = GraphQL::Boolean.new;

or just use $GraphQLBoolean.

GraphQL::ID is GraphQL::Scalar

Core ID type, maps to Perl type ID which is a subset of Cool.

You can create your own:

my $ID = GraphQL::ID.new;

or just use $GraphQLID.

GraphQL::EnumValue is GraphQL::Scalar does Deprecatable

The individual enumerated values of an Enum, represented as quoted strings in JSON.

my $enumvalue = GraphQL::EnumValue.new(name => 'SOME_VALUE');

They can also be deprecated:

my $enumvalue = GraphQL::EnumValue.new(name => 'SOME_VALUE',
                                       :isDeprecated,
                                       reason => 'Just because');

or can be later deprecated:

$enumvalue.deprecate('Just because');

See GraphQL::Enum for more information about creating EnumValues.

GraphQL::List is GraphQL::Type

.kind() = ‘LIST’, and has $.ofType with some other GraphQL::Type.

my $list-of-strings = GraphQL::List.new(ofType => $GraphQLString);

In GSL, Lists are represented by wrapping another type with square brackets ‘[’ and ‘]’. e.g.

[String]

GraphQL::Non-Null is GraphQL::Type

By default GraphQL types can all take on the value null (in Perl, Nil). Wrapping them with Non-Null disallows the null.

.kind() = ‘NON_NULL’

my $non-null-string = GraphQL::Non-Null.new(ofType => $GraphQLString);

In GSL, Non-Null types are represented by appending an exclation point, ‘!’. e.g.

String!

To define a Perl class with a non-null attribute, both add the :D type constraint to the type, and also specify it as is required (or give it a default). To mark a type in a method as non-null, append with an exclamation point. e.g.

class Something
{
    has Str:D $.my is rw is required;

    method something(Str :$somearg! --> ID) { ... }
}

GraphQL::InputValue is GraphQL::Type

The type is used to represent arguments for GraphQL::Fields and Directives arguments as well as the inputFields of a GraphQL::InputObjectType. Has a $.type attribute and optionally a $.defaultValue attribute.

my $inputvalue = GraphQL::InputValue.new(name => 'somearg',
                                         type => $GraphQLString,
                                         defaultValue => 'some default');

in GSL:

somearg: String = "some default"

in Perl:

Str :$somearg = 'some default'

GraphQL::Field is GraphQL::Type does Deprecatable

In addition to the inherited .name, .description, .isDeprecated, .deprecationReason, has attributes .args which is an array of GraphQL::InputValues, and .type which is the type of this field. Since the Field is the place where the Schema connects to resolvers, there is also a .resolver attribute which can be connected to arbitrary code. Much more about resolvers in Resolution below.

my $field = GraphQL::Field.new(
   name => 'myfield',
   type => $GraphQLString,
   args => GraphQL::InputValue.new(
               name => 'somearg',
               type => $GraphQLString,
               defaultValue => 'some default'),
   resolver => sub { ... });

In GSL:

myfield(somearg: String = "some default"): String

In Perl:

method myfield(Str :$somearg = 'some default' --> Str) { ... }

Note that as a strongly, staticly typed system, every argument must be a named argument, and have an attached type (a valid one in the list above that map to GraphQL types), and the return must specify a type.

You can deprecate by setting the attributes .isDeprecated and optionally .deprecationReason or using the GSL @deprecate directive described below.

GraphQL::Interface is GraphQL::Type does HasFields

In addition to the inherited $.name, $.description, and @.fieldlist, also has the attribute @.possibleTypes with the list of object types that implement the interface. You needn’t set @.possibleTypes, as each GraphQL::Object specifies which interfaces they implement, and the Schema finalization will list them all here.

my $interface = GraphQL::Interface.new(
   name => 'myinterface',
   fieldlist => (GraphQL::Field.new(...), GraphQL::Field.new(...))
);

In GSL:

interface myinterface {
  ...fields...
}

GraphQL::Object is GraphQL::Type does HasFields

In addition to the inherited $.name, $.description, and @.fieldlist, also has the attribute @.interfaces with the interfaces which the object implements, and the .kind() method which always returns ‘OBJECT’.

my $obj = GraphQL::Object.new(
   name => 'myobject',
   interfaces => ($someinterface, $someotherinterface),
   fieldlist => (GraphQL::Field.new(...), GraphQL::Field.new(...))
);

In GSL:

type myobject implements someinterface, someotherinterface {
  ...fields...
}

In Perl:

class myobject {
    ...fields...
}

NOTE: Interfaces aren’t yet implemented for the perl classes.

GraphQL::InputObjectType is GraphQL::Type

Input Objects are object like types used as inputs to queries. Their .kind() method returns ‘INPUT_OBJECT’. They have a @.inputFields array of GraphQL::InputValues, very similar to the fields defined within a normal Object.

my $obj = GraphQL::InputObjectType.new(
   name => 'myinputobject',
   inputFields => (GraphQL::InputValue.new(...), GraphQL::InputValue.new(...)
);

In GSL:

input myinputobject {
   ...inputvalues...
}

In Perl, you must specify a class explicitly as a GraphQL::InputObject:

class myinputobject is GraphQL::InputObject {
   ...inputvalues...
}

GraphQL::Union is GraphQL::Type

A union has .kind() = ‘UNION’, and a @.possibleTypes attribute listing the types of the union.

my $union = GraphQL::Union.new(
   name => 'myunion',
   possibleTypes => ($someobject, $someotherobject)
);

In GSL:

union myunion = someobject | someotherobject

NOTE: Not yet implemented in Perl classes.

GraphQL::Enum is GraphQL::Type

Has .kind() = ‘ENUM’, and @.enumValues with a list of GraphQL::EnumValues. The accessor method for .enumValues() takes an optional Bool argument :$includeDeprecated which will either include deprecated values or exclude them.

my $enum = GraphQL::Enum.new(
   name => 'myenum',
   enumValues => (GraphQL::EnumValue.new(...), GraphQL::EnumValue.new(...))
);

In GSL:

enum myenum { VAL1 VAL2 ... }

In Perl:

enum myenum <VAL1 VAL2 ...>;

GraphQL::Directive is GraphQL::Type

Still needs work…