Working with GraphQL type system
A central point of each GraphQL service is its schema. Schema defines a namespace for type definitions exposed to service consumers, that can be used later to generate things such as documentation, introspection queries and finally to execute and validate incoming GraphQL queries and mutations.
Each schema definition requires to have something called root query - it's a GraphQL object definition, which exposes all top level fields to be requested by the user. Aside of it there are also separate roots for mutations (queries are readonly requests) and subscriptions (which is an experimental feature, at the moment of v0.2-beta is not yet supported by Fsharp.Data.GraphQL).
GraphQL type system categorizes several custom types, that can be defined by programmer, including:
Beside them, FSharp.Data.GraphQL defines two others:
- Lists - which can be used to compose types defined above in context of collections.
- Nullables - which can be used to define potentially absent fields in form of the F# option types. This differs from GraphQL standard, where fields are nullable by default and can be optionally marked as non-null. Such approach however wouldn't fit the spirit of FSharp programming language.
One of the important distintions, that can save your time in the future is difference between Input and Output type definitions:
-
Output types define valid types that can be produced as GraphQL query response. This includes:
- Objects
- Interfaces
- Unions
- Enums
- Scalars
- Lists
- Nullables
-
Input types define types, which can be used as valid values included in GraphQL query itself or as set of attached variables. This includes:
- InputObjects
- Enums
- Scalars
- Lists
- Nullables
Please, note the distinction between those two - not all type definitions are both valid inputs and outputs.
Defining an Object
Objects are the most common GraphQL type definitions. They describe a complex value with a set of well-defined fields, i.e:
1: 2: 3: |
|
You can enhance most of the GraphQL components with description (this includes both object and fields definitions) - it later can be queried and used by tools like graphiql to be used as documentation.
Fields can also be parametrized - by specifying a list of arguments in GraphQL field definition you can use them later to pass runtime parameters, that may differ with each query, even when query structure remains the same. Do define an argument use Define.Input
helper method:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: |
|
Defining recursive type references
Sometimes you may find hard to define a GraphQL types having recursive relationship to each other. Good example of that would be a Person
object with field friends
returning list of instances of type Person
itself. It's hard to do so due to limitations of F# language. However you can still achieve that by using overloaded Define.Object
method:
1: 2: 3: |
|
As you may see, we defined Person object definition using rec keyword and instead of defining fields as a list and we used a lazily evaluated function instead.
Defining an Interface
GraphQL interfaces are so called abstract types (along with unions). This means, that they can be used as part of the query, however query materialization must always be bound to some concrete Object type definition.
Object definitions must explicitly implement interfaces and all of their fields in order to use them:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: |
|
What's worth noticing here is that GraphQL interface definitions are not necessarily bound to .NET interfaces. You can create an interface definition (like the one above) which will be known only to GraphQL schema.
Once you've defined a schema, you can retrieve all definitions implementing target interface, by calling schema.GetPossibleTypes(Animal)
.
Defining an Union
GraphQL unions are basically enumerations of one of the object definitions. Unlike F# discriminated unions, they work on existing object types, that can be used on their own. This makes them initially heavy to work with.
However with FSharp.Data.GraphQL, it is possible combine existing F# types with discriminated unions under the GraphQL union definition:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: |
|
The example above shows, how you can use Pet
discriminated union as a proxy to being able to define GraphQL union type and still being able to operate with .NET type system in safe manner.
Defining an Enum
GraphQL enums can be quite closelly related to C# enums - they define primitive types (used as GraphQL leaf types) that have strictly defined set of possible values, i.e:
1: 2: 3: 4: 5: 6: 7: 8: 9: |
|
The major difference from .NET here is that GraphQL expects, that enum values are serialized as strings. Therefore, upon serialization, given enum value will be projected using ToString()
method.
Defining a Scalar
Just like enums, GraphQL scalars are leaf types, that should be able to be serialized/deserialized into primitive types supported by format of your choice (which will be JSON in most of the cases).
1: 2: 3: 4: |
|
This examples shows how to create a scalar definition for .NET Guid
type. It requires two functions to be defines:
coerceInput
function, which will be used to resolve your scalar value directly from values encoded in GraphQL query string (in this case StringValue is just part of parsed query AST).coerceValue
function, which will be used to apply something like "implicit casts" between two .NET types in conflicting cases - like when value is passed in variables query string part - which should be tolerated.
Defining an Input Object
Just like objects, input objects describe a complex data types - however while Objects are designed to work as output types, Input Objects are input types only.
1: 2: 3: 4: |
|
Unlike the objects, you neither define input object field resolver nor provide any arguments for it. They also don't work together with abstract types like interfaces or unions.
Full name: typesystem.Person
from Microsoft.FSharp.Core
Full name: typesystem.pageSize
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.query
from Microsoft.FSharp.Collections
Full name: Microsoft.FSharp.Collections.Seq.toList
Full name: typesystem.Animal
type InterfaceAttribute =
inherit Attribute
new : unit -> InterfaceAttribute
Full name: Microsoft.FSharp.Core.InterfaceAttribute
--------------------
new : unit -> InterfaceAttribute
Full name: Microsoft.FSharp.Core.obj
Full name: typesystem.Cat
Full name: typesystem.Dog
val Cat : obj
Full name: typesystem.Cat
--------------------
type Cat =
{Name: string;
Meows: bool;}
Full name: typesystem.Cat
val string : value:'T -> string
Full name: Microsoft.FSharp.Core.Operators.string
--------------------
type string = System.String
Full name: Microsoft.FSharp.Core.string
Full name: Microsoft.FSharp.Core.bool
val Dog : obj
Full name: typesystem.Dog
--------------------
type Dog =
{Name: string;
Barks: bool;}
Full name: typesystem.Dog
| DogPet of Dog
| CatPet of Cat
Full name: typesystem.Pet
val Pet : obj
Full name: typesystem.Pet
--------------------
type Pet =
| DogPet of Dog
| CatPet of Cat
Full name: typesystem.Pet
Full name: Microsoft.FSharp.Core.Operators.box
| Gt = 1
| Eq = 0
| Lt = -1
Full name: typesystem.Ord
val Ord : obj
Full name: typesystem.Ord
--------------------
type Ord =
| Gt = 1
| Eq = 0
| Lt = -1
Full name: typesystem.Ord
Full name: typesystem.Guid
{Email: string;
Password: string;}
Full name: typesystem.CreateAccountData
val CreateAccountData : obj
Full name: typesystem.CreateAccountData
--------------------
type CreateAccountData =
{Email: string;
Password: string;}
Full name: typesystem.CreateAccountData