Chapter 4 GraphQL: Objects

4.1 Introduction

The GraphQL specification includes the following default scalar types: Int, Float, String, Boolean and ID. While this covers most of the use cases, often you need to support custom atomic data types (e.g. Date), or you want a version of an existing type that does some validation. To enable this, GraphQL allows you to define custom scalar types. Enumerations are similar to custom scalars, but their values can only be one of a pre-defined list of strings.

The way to define new scalars or enums in the schema is shown below:

scalar MyCustomScalar

enum Direction {
  NORTH
  EAST
  SOUTH
  WEST
}

type MyType {
    myAttribute: MyCustomScalar
    direction: Direction
    ...
}

Fields can take arguments as input. These can be used to customize the return value (eg, filtering search results). This is known as field argument.

If you have a look at our schema.graphql you can find an example of usage of a field argument for attribute actors in type Movie. The total argument is used to define the max number of actors returned from the server.

4.2 Code

4.2.1 Enum

Library github.com/99designs/gqlgen generates golang code from the graphql schema. This library makes use of a configuration file gqlgen.yml in the root directory. Every time we make a change in the graphql.schema or in th gqlgen.yml we need to re-generate the code.

Generated code by 99designs/gqlgen mustn’t be modified manually.

To generate the code we run the following command

make generate

or

go run github.com/99designs/gqlgen

For example, for the enum Direction, the generated code would be

./graphql/model/generated.go

// PossibleValuesForGender
type Direction string

const (
    DirectionNorth  Direction = "NORTH"
    DirectionEast   Direction = "EAST"
    DirectionSouth  Direction = "SOUTH"
    DirectionWest   Direction = "WEST"
)

var AllDirection = []Direction{
    DirectionMale,
    DirectionFemale,
}

func (e Direction) IsValid() bool {
    switch e {
    case DirectionNorth, DirectionEast, DirectionSouth, DirectionWest :
        return true
    }
    return false
}

func (e Direction) String() string {
    return string(e)
}

func (e *Direction) UnmarshalGQL(v interface{}) error {
    str, ok := v.(string)
    if !ok {
        return fmt.Errorf("enums must be strings")
    }

    *e = Direction(str)
    if !e.IsValid() {
        return fmt.Errorf("%s is not a valid Direction", str)
    }
    return nil
}

func (e Direction) MarshalGQL(w io.Writer) {
    fmt.Fprint(w, strconv.Quote(e.String()))
}

4.2.2 Resolvers

Let’s imagine that we have an operation that returns a Employee. Employee contains an attribute details of type SocialDetails whose information needs to be taken from an external API. And this attribute won’t be always required by the API consumers.

Server should not waste time on obtaining something that clients do not need.

resources/graphql/workshop.graphql

enum Source{
    Linkedin
    Facebook
}
type Person {

  details:SocialDetails
}

type Query {
    listPeople:[Person!]!
}

graphql/model/models.go

type Person struct {
    //Person table has two attributes: Id and FullName
    Person database.PersonTable 
}

type SocialDetails struct {
    Profile   string
    Refrences []string
}

gqlgen.yml

We add a new entry for model Person

...
models:
  Person:
    model: workshop-graphql-go/graphql/model.Person
...

graphql/resolver/person.go

type PersonResolver struct{}

... 
    
func (r *PersonResolver) Details(ctx context.Context, obj *model.Person) (model.SocialDetails, error) {
    linkedin:= GetHttpClien(ctx).GetLinkedinSocialDetails()
    return model.SocialDetails{Profile:linkedin.Profile, References:linkeding.References},nil
}

...

Now, let’s imagine that SocialDetails can be taken from more than one social network. And consumers can decide which social network will be used.

schema.graphql.

enum Source{
    Linkedin
    Facebook
}
type Person {
  details(source:Source=Linkedin):SocialDetails
}
type Query {
    listPeople:[Person!]!
}

gqlgen.yml

...
models:
  Person:
    model: workshop-graphql-go/graphql/model.Person
...
type Person struct {
    // Person table has two attributes: Id and FullName
    Person database.PersonTable 
}

type SocialDetails struct {
    Profile   string
    Refrences []string
}

graphql/resolver/person.go

type PersonResolver struct{}

... 
    
func (r *PersonResolver) SocialDetails(ctx context.Context, obj *model.Person, source *model.Source) (model.SocialDetails, error) {
    if source==model.Linkedin{
        linkedin:= GetHttpClien(ctx).GetLinkedinSocialDetails()
        return model.SocialDetails{Profile:linkedin.Profile, References:linkeding.References},nil
    } else if source==model.Facebook{
        facebook:= GetHttpClien(ctx).GetFacebookSocialDetails()
        return model.SocialDetails{Profile:facebook.Profile, References:facebook.References},nil
    }
    return nil,errors.New("Unexpected social source")
}

...

4.2.3 Scalars

To define a new scalar in Golang is very straightforward. We just need to define a type and then implement the following functions:

  • UnmarshalGQL(input interface{}) error
  • MarshalGQL(w io.Writer)

Below a custom scalar type implementation for odd numbers.

./graphql/scalar/odd.go

import (
    "fmt"
    "io"
    "net/url"
    "strconv"
)

type Odd string

func (o *Odd) UnmarshalGQL(input interface{}) error {
    switch input := input.(type) {
    case int32:
        if(int32(input)%2==in){
            *j = int32(input)
            return nil
        }
        return fmt.Errorf("Even numbers are not allowed")
        
    default:
        return fmt.Errorf("wrong type")
    }
}

// MarshalGQL implements the graphql.Marshaler interface
func (y Odd) MarshalGQL(w io.Writer) {
    w.Write([]byte(strconv.Quote(string(y))))

}

gqlgen.yml

We need to add a new entry for the scalar type.

...
models:
  ...
  Odd:
    model: workshop-graphql-go/graphql/scalar.Odd

4.3 Challenges

  1. Define an enum type Genre whose values are Drama and SciFi (add as many other as you want) and use it for attribute genre in type Movie and MovieRequest.

Run this query to verify your implementation works as expected

mutation {
  addMovie (request:{
    title: "Corpse Bride"
    year: 2005
    budget: 35000000
    directorId: 1
    genre: SciFi
    trailer: "https://www.youtube.com/watch?v=o5qOjhD8j08"
  }){
    id
    director{
      fullName
      country
    }
    genre
  }
}
  1. Define an enum Gender and use it for attribute gender in type Actor.

Run this query to verify your implementation works as expected

query {
  listActors{
    fullName
    gender
  }
}
  1. Define a scalar type Url and use it in attribute trailer of Movie and MovieRequest. Only valid url’s should be permitted.

To check if url is valid you could do

if _, err := url.ParseRequestURI(input); err != nil {
    fmt.Println(err.Error())
    return fmt.Errorf(err.Error())
}

Run this query to verify that only valid url’s are permitted. Actually, the movie should not be saved into the database.

mutation {
  addMovie (request:{
    title: "Gran Torino"
    year: 2009
    budget: 28000000
    directorId: 6
    genre: Drama
    trailer: ".http"
  }){
    id
    director{
      fullName
      country
    }
    genre
    trailer
  }
}

Run this query to verify that your scalar Url works as expected.

mutation {
  addMovie (request:{
    title: "Gran Torino"
    year: 2009
    budget: 28000000
    directorId: 6
    genre: Drama
    trailer: "https://www.youtube.com/watch?v=9ecW-d-CBPc"
  }){
    id
    director{
      fullName
      country
    }
    genre
    trailer
  }
}
  1. Define an enum type Currency whose possible values are Euro and Dollar. Our API must permit the API consumers to decide in which currency they want to obtain attribute budget in type Movie. 1€ => 1.14$

Run this query

query {
  getMovie(movieId:1){
    budgetInEuros: budget(currency:Euro)
    budgetInDollars: budget(currency:Dollar)
  }
}

and verify that the output should be this:

{
  "data": {
    "getMovie": {
      "budgetInEuros": 20,
      "budgetInDollars": 22.799999999999997
    }
  }
}