Skip to content

Latest commit

 

History

History
817 lines (691 loc) · 24.1 KB

day_05.md

File metadata and controls

817 lines (691 loc) · 24.1 KB

Day 5: Interfaces and Unions

  • Interface
    • Inline fragments
    • Meta Fields
  • Union
  • Exercise
  • Learning resources

If you are here and followed the previous lessons you pretty much covered the basics of GraphQL, most small projects won't really need what's next, but they certainly will as soon as you have to scale.

Interfaces

Underestimating things is dangerous, especially when it comes to software engineering, and particularly when the same term is used across technologies and you're tempted to assume they work just like the other one works. Interfaces is one of these cases.

Let's start with the spec and gradually break it down and understand how the "Interface" has similarities and differences from e.g. OOP Interface implementation in Java or other language.

GraphQL interfaces represent a list of named fields and their arguments. GraphQL objects can then implement these interfaces which requires that the object type will define all fields defined by those interfaces.

Fields on a GraphQL interface have the same rules as fields on a GraphQL object; their type can be Scalar, Object, Enum, Interface, or Union, or any wrapping type whose base type is one of those five.

Source: GraphQL spec - June 2018 - Interfaces

Interfaces are an abstract type where there are common fields declared. Any type that implements an interface must define all the fields with names and types exactly matching.

Source: GraphQL spec - June 2018 - Interface

So far is pretty much the same concept you'll see in OOP:

you'll might be tempted to replace the Type word with Class for a mental map but I'd discourage that, it might mislead you

  • A common shape abstracted in an Interface without the concrete implementation
  • Many different Types, having a shape intersection (common fields) and individual specific characteristics (distinctive fields) which can be identified as individuals AND as part of a wider group, can implement the common Interface (this is a design decision, having common fields doesn't mean they're necessarily part of a group and sharing and interface)
  • The Interface cannot be used directly but only through Types implementing it
  • The Types must implement all fields defined in the Interface

In a concrete example we'll start seeing how GraphQL Interfaces are similar to OOP and how they're not.

We started with the following example:

input InputCharacter {
  homeland: String
  kind: Kind
  skill: String
}
input InputCharacterUpdateHomelandByKind {
  homeland: String!
  kind: Kind!
}

enum Kind {
  HOBBIT
  ELVEN
  HALF_ELVEN
  ISTARI
}

type Character {
  id: ID
  name: String
  surname: String
  age: Int
  homeland: String
  kind: Kind
  friends (input: InputCharacter): [Character!]
  progenitor (input: InputCharacter): [Character!]
  skill: String
}

type Query {
  characters (input: InputCharacter): [Character]
}

But what if later on, we have the need to distinguish the characters? Can we do that in a backwards compatible way? (remember, a "breaking change" is like Chucky, Freddy Krueger, CandyMan and COVID-19 all knocking at your door whilst the Bogyman is coming down the chimney in a Santa's suit ... you ain't do it).

So?

We change the Type Character into an Interface and create the new types implementing it.

interface Character {
  id: ID
  name: String
  surname: String
  age: Int
  homeland: String
  kind: Kind
  friends (input: InputCharacter): [Character!]
  progenitor (input: InputCharacter): [Character!]
  skill: String
}

type Hobbit implements Character {
  id: ID
  name: String
  surname: String
  age: Int
  homeland: String
  kind: Kind
  friends (input: InputCharacter): [Character!]
  progenitor (input: InputCharacter): [Character!]
  skill: String
  ## Specific Hobbit props
  mathoms: [String!]
}

type Elvish implements Character {
  ... ## for brevity imagine all the Character fields here
  ## Specific Elvish props
  ageLess: Boolean
}

type Istari implements Character {
  ... ## for brevity imagine all the Character fields here
  ## Specific Istari props
  maiarName: String!
}

type Query {
  characters (input: InputCharacter): [Character]
}

At this point you might start making questions like:

  1. Should I have to repeat all common fields?
    • Yes, (◔_◔) so annoying, I know, but remember, you're implementing an interface, you'll see in the next 2 questions how that's relevant.
  2. I'm still seeing Character directly referenced as Output type on the characters query operation. Is that correct?
    • Yup, that's great though! whoever is using the API won't have a breaking change, it'll be transparent!
  3. Oh, so where the heck the "cannot be directly used" thing fits here?
    • (⌐■_■) Exactly !!!!! remember every field in a type will eventually execute a resolver function either explicit or implicit (default)? There you go, the disambiguation happens at resolver level and there's where you end up not-using the Interface directly.

I know, the documentation is not really enlighten on this topic, furthermore, there's not a definition on how to resolve an abstract type on the spec, concretely because is a concern of the server implementation to deal with that.

On chapter 6.4.3 Value Completion of the June2018 spec we can read:

After resolving the value for a field, it is completed by ensuring it adheres to the expected return type. If the return type is another Object type, then the field execution process continues recursively. ...

  • CompleteValue(fieldType, fields, result, variableValues) ....
    • Let objectType be ResolveAbstractType(fieldType, result)

and

Resolving Abstract Types

When completing a field with an abstract return type, that is an Interface or Union return type, first the abstract type must be resolved to a relevant Object type. This determination is made by the internal system using whatever means appropriate.

Note: A common method of determining the Object type for an objectValue in object‐oriented environments, such as Java or C#, is to use the class name of the objectValue.

  • ResolveAbstractType(abstractType, objectValue)
    • Return the result of calling the internal method provided by the type system for determining the Object type of abstractType given the value objectValue.

... but that's still delegating the concrete resolution to the server implementation.

We'll see how to do it with Apollo due to it's simplicity, taking in consideration other server implementations might defer but they're very similar.

Originally, before adding the interface, we had this:

const resolvers = {
  Kind: {
    // our Kind enum values mapping
  },
  Query: {
    characters(obj, args, context, info) {
      // our characters operation body
    }
  },
  Character: {
    friends(obj, args, context, info) {
      // our field resolver body
    },
    progenitor(obj, args, context, info) {
      // our  field resolver body
    }
  },
  Mutation: {
    /// our mutation operations
  }
}

But now we can't use the Character fields resolvers, it's an Interface, hence we need first to identify the concrete Type and then let GraphQL know that because it doesn't know it at this point. I know it's not really beautiful (it might feel like going back to the dark ages) but the common way is to rely on what makes each type different from the others based on the shape, it'll vary depending on the server implementation, the language and in the end, the team writing the app.

const resolvers = {
  Kind: {
    // our Kind enum values mapping
  },
  Query: {
    characters(obj, args, context, info) {
      // our characters operation body
    }
  },
  Character: {
    __resolveType(obj, context, info, resolveType) {

      const {
        maiarName,
        mathoms,
        ageLess
      } = obj;

      if (mathoms) {
        return 'Hobbit';
      }

      if (ageLess) {
        return 'Elvish';
      }

      if (maiarName) {
        return 'Istari';
      }

    },
  },
  Hobbit: {
    friends, // field resolver
    progenitor, // field resolver
  },
  Elvish: {
    friends, // field resolver
    progenitor, // field resolver
  },
  Istari: {
    friends, // field resolver
    progenitor, // field resolver
  },
  Mutation: {
    /// our mutation operations
  }
}

I'm oversimplifying this, but if you see how it's described on Apollo's documentation is pretty much that.

Now in order to make a query operation and get the specific values for each type we have to use the same old props for the common fields and inline fragments for the type specific ones and you may also add a specific meta field __typename to know what's the type of each record:

query Characters{
  characters {
    __typename
    name
    homeland
    ... on Hobbit {
      mathoms
    }
    ... on Elvish {
      ageLess
    }
    ... on Istari {
      maiarName
    }
  }
}

This is what you'll get:

{
  "data": {
    "characters": [
      {
        "__typename": "Hobbit",
        "name": "Frodo",
        "homeland": "The Shire",
        "mathoms": [
          "spoon",
          "lamp"
        ]
      },
      {
        "__typename": "Hobbit",
        "name": "Sam",
        "homeland": "The Shire",
        "mathoms": [
          "shovel",
          "bucket"
        ]
      },
      {
        "__typename": "Hobbit",
        "name": "Meriadoc",
        "homeland": "Buckland",
        "mathoms": [
          "table",
          "chairs"
        ]
      },
      {
        "__typename": "Hobbit",
        "name": "Peregrin",
        "homeland": "The Shire",
        "mathoms": [
          "door"
        ]
      },
      {
        "__typename": "Elvish",
        "name": "Arwen",
        "homeland": "Rivendell",
        "ageLess": true
      },
      {
        "__typename": "Elvish",
        "name": "Elrond",
        "homeland": "Rivendell",
        "ageLess": true
      },
      {
        "__typename": "Elvish",
        "name": "Celebrían",
        "homeland": "Valinor",
        "ageLess": true
      },
      {
        "__typename": "Hobbit",
        "name": "Drogo",
        "homeland": "The Shire",
        "mathoms": []
      },
      {
        "__typename": "Hobbit",
        "name": "Saradoc",
        "homeland": "Buckland",
        "mathoms": []
      },
      {
        "__typename": "Hobbit",
        "name": "Esmeralda",
        "homeland": "Buckland",
        "mathoms": []
      },
      {
        "__typename": "Istari",
        "name": "Gandalf",
        "homeland": "",
        "maiarName": "Olórin"
      },
      {
        "__typename": "Istari",
        "name": "Saruman",
        "homeland": "",
        "maiarName": "Curumo"
      },
      {
        "__typename": "Istari",
        "name": "Radagast",
        "homeland": "",
        "maiarName": "Aiwendil"
      },
      {
        "__typename": "Elvish",
        "name": "Galadriel",
        "homeland": "Lothlorien",
        "ageLess": true
      }
    ]
  }
}

Alternatively you can query which types are implementing a certain Interface using Schema Introspection:

query UnionInterfaceTypes {
  __type(name: "Character") {
    possibleTypes {
      name
      kind
    }
  }
}
{
  "data": {
    "__type": {
      "possibleTypes": [
        {
          "name": "Hobbit",
          "kind": "OBJECT"
        },
        {
          "name": "Elvish",
          "kind": "OBJECT"
        },
        {
          "name": "Istari",
          "kind": "OBJECT"
        }
      ]
    }
  }
}

So far so good? Let's make it a little harder.

Ready?

A type can implement multiple interfaces

You may say "yeah... (◔_◔) whatever, piece of cake".

Not so fast!

## we add a new interface
interface MagicalCreature {
  magicPowers: [String!]
}

### and update 2 types to implement it


type Elvish implements Character & MagicalCreature{
  ... ## all previous fields remain unchanged
  ## the new field
  magicPowers: [String!]
}

type Istari implements Character & MagicalCreature{
  ... ## all previous fields remain unchanged
  ## the new field
  magicPowers: [String!]
}

type Query {
  ... ## all previous query operations
  magical: [MagicalCreature]
}

Now the resolvers

const resolvers = {
  Kind: {
    // our Kind enum values mapping
  },
  Query: {
    characters(obj, args, context, info) {
      // our operation body
    },
    magical(obj, args, context, info) {
      // our operation body
    }
  },
  Character: {
    __resolveType({
      maiarName,
      mathoms,
      ageLess
    }, context, info, returnType) {

      if (mathoms) {
        return 'Hobbit';
      }

      if (ageLess) {
        return 'Elvish';
      }

      if (maiarName) {
        return 'Istari';
      }

    },
  },
  MagicalCreature: {
    __resolveType({
      maiarName,
      ageLess
    }, context, info, returnType) {

      if (ageLess) {
        return 'Elvish';
      }

      if (maiarName) {
        return 'Istari';
      }

    },
  },
  Hobbit: {
    friends, // field resolver
    progenitor, // field resolver
  },
  Elvish: {
    friends, // field resolver
    progenitor, // field resolver
  },
  Istari: {
    friends, // field resolver
    progenitor, // field resolver
  },
  Mutation: {
    /// our mutation operations
  }
}

As you can see, we start having a LOT of repeated code, and because of the __resolveType implementation forcing you to return a string with the name of a type, you might end having to deal with the return order (probably due to a "not very refined" design on your type definition). A lot of attention and planning is required to avoid derailing here, and trust me, it can happen really fast.

Unions

Unions are an abstract type where no common fields are declared. The possible types of a union are explicitly listed out in possibleTypes. Types can be made parts of unions without modification of that type.

Source: GraphQL spec - June 2018 - Union

GraphQL Unions represent an object that could be one of a list of GraphQL Object types, but provides for no guaranteed fields between those types. They also differ from interfaces in that Object types declare what interfaces they implement, but are not aware of what unions contain them.

With interfaces and objects, only those fields defined on the type can be queried directly; to query other fields on an interface, typed fragments must be used. This is the same as for unions, but unions do not define any fields, so no fields may be queried on this type without the use of type refining fragments or inline fragments.

Source: GraphQL spec - June 2018 - Unions

Let's summarize Unions so we can concentrate on the hot stuff :)

  • Abstract types
  • Represent a list of arbitrary Object types
  • No common fields are declared
  • Provides for no guaranteed fields between Object Types listed in a Union type
  • Object types not aware of unions containing them.
  • Being an abstract type implies disambiguation happening at runtime on your resolvers (depending on the technology but usually achieved as Interfaces do, e.g. __resolveType(...)).

Declaring a Union:

union MyUnion = ObjectA | ObjectB | ObjectC

Also Union members may be defined with an optional leading | character to aid formatting when representing a longer list of possible types:

union MyUnion =
  | ObjectA
  | ObjectB
  | ObjectC
  | ObjectD
  | ObjectE

Some insights before moving forward:

What's the point of having a type for potentially unrelated Objects?

Let's say, in our LOTR example, you want to create a global search query operation for your blog.

You'll have:

  • Objects sharing a common interface like Character
  • Objects with shared fields (e.g. name) but not sharing a logical hierarchy like Sword, Realm, City, Author, Reviewer, User
  • Objects with completely different shapes like Review, Comment

Doing something like this is completely valid!

union GlobalSearchResult =
  | Character
  | Sword
  | Realm
  | City
  | Author
  | Reviewer
  | User
  | Review
  | Comment

type Query {
    globalSearch: [GlobalSearchResult]
}

and

const resolvers = {
  GlobalSearchResult: {
    __resolveType(obj, context, info, returnType){

      if (obj.bladeLength){
        return 'Sword';
      }
      /// and so on
    },
  },
  Query: {
    globalSearch: () => { ... }
  },
};

Can I combine a Union and an Interface to guarantee an intersection?

Technically yes.

interface Searchable {
  name: String
}

union GlobalSearchResult =
  | Character ## implements Searchable
  | Sword ## idem
  | Realm ## idem
  | City ## idem
  | Author ## idem
  | Reviewer ## idem
  | User ## idem
  | Review ## OOPS, what here?
  | Comment ## OOPS, what here?

type Query {
    globalSearch: [GlobalSearchResult]
}

You stated every Searchable Object should implement the name property, ... that's fine, it's declarative, but what's the point of having a Union here? You could make it completely restrictive and get rid of the union entirely ... but ... you'll miss the Review and Comment because it doesn't make any sense to have a name property there at all. ...

Exercise

For a given datasource (abstracted as json here) containing n rows of skills and n rows of persons we provided a sample implementation of a GraphQL server for each technology containing:

  • a server app
  • a schema
  • a resolver map
  • an entity model
  • a db abstraction

The code contains the solution for previous exercises so you can have a starting point example.

Exercise requirements

This is a long one, keep it simple and put all your attention on the GraphQL aspects. Unless you're blocked, write down your questions so you can have the opportunity to discuss them later.

Schema:

  • Add a Grade enum with some values of your choice (e.g. TRAINEE, JUNIOR, SENIOR)
  • Add a Role enum with some values of your choice (e.g. DEVELOPER, SDET, TEAM_LEAD)
  • Change Person type to an Interface
  • Add an Employee Interface with a non-nullable employeeId property of type ID
  • Add a Contact type implementing Person interface with no extra properties
  • Add a Candidate type implementing Person interface with 2 extra properties
    • non-nullable targetRole of type Role
    • non-nullable targetGrade of type Grade
  • Add an Engineer type implementing Person and Employee interfaces with 2 extra properties
    • non-nullable role of type Role
    • non-nullable grade of type Grade
  • Add a GlobalSearch union listing Skill, Contact, Candidate and Engineer object types
  • Add an InputCandidateCreate input with the same properties InputPersonCreate input has with some changes:
    • surname, email, age should be non-nullable
    • add non-nullable targetRole of type Role
    • add non-nullable targetGrade of type Grade
  • Add an InputEngineerCreate input with the same properties InputPersonCreate input has with some changes:
    • surname, email, age should be non-nullable
    • add non-nullable role of type Role
    • add non-nullable grade of type Grade
  • Add an InputGlobalSearch input with a non-nullable name property of type String
  • Update the InputPerson input with the following properties:
    • nullable targetRole of type Role
    • nullable targetGrade of type Grade
    • nullable role of type Role
    • nullable grade of type Grade
  • Add a search query operation with an input argument of type InputGlobalSearch which returns a nullable list of GlobalSearch union types
  • Add a createCandidate mutation operation with an input argument of type InputCandidateCreate which returns a non-nullable Candidate object types
  • Add a createEngineer mutation operation with an input argument of type InputEngineerCreate which returns a non-nullable Engineer object types

Resolvers:

  • Add a createCandidate mutation resolver (use the createPerson resolver as a guide)
  • Add a createEngineer mutation resolver (use the createPerson resolver as a guide)
  • Add a GlobalSearch Root Operation definition with a type resolver to return the right Object type listed in the union type. This will change a lot depending on the language and the server implementation, e.g. with Apollo Server it's the __resolverType resolver.
  • Add Person and Employee Root Operation definitions with a type resolver to return the right Object type for the Object types implementing the interfaces. This will change a lot depending on the language and the server implementation, e.g. with Apollo Server it's the __resolverType resolver.
  • Add Contact, Candidate, and Engineer Root Operation definitions with the required field resolvers (hint: they were initially on the Person Root Operation before changing it to an interface).
  • Add a search query operation which returns the matching results on the persons and skills records (take a look at the Person and Skill models, they should provide a functionality to perform the concrete search)

Extra considerations

  • All previous operations (from previous days) MUST still work
  • All queries related to the Person interface can now support inline fragments to request fields for specific types, try some options and see what happens (e.g. on the globalSearch operation).
  • Don't bother about validations except for those provided by GraphQL and only GraphQL.

Operations list

## Part 1

mutation createCandidate(
  $name: String!
  $surname: String!
  $email: String!
  $age: Int!
  $eyeColor: EyeColor
  $friends: [ID!]
  $skills: [ID!]
  $favSkill: ID
  $targetRole: Role!
  $targetGrade: Grade!
) {
  createCandidate(
    input: {
      name: $name
      surname: $surname
      email: $email
      age: $age
      eyeColor: $eyeColor
      friends: $friends
      skills: $skills
      favSkill: $favSkill
      targetRole: $targetRole
      targetGrade: $targetGrade
    }
  ) {
    id
    fullName
    email
    age
    eyeColor
    targetRole
    targetGrade
  }
}

## Part 2

mutation createEngineer(
  $name: String!
  $surname: String!
  $email: String!
  $age: Int!
  $eyeColor: EyeColor
  $friends: [ID!]
  $skills: [ID!]
  $favSkill: ID
  $role: Role!
  $grade: Grade!
) {
  createEngineer(
    input: {
      name: $name
      surname: $surname
      email: $email
      age: $age
      eyeColor: $eyeColor
      friends: $friends
      skills: $skills
      favSkill: $favSkill
      role: $role
      grade: $grade
    }
  ) {
    id
    employeeId
    fullName
    email
    age
    eyeColor
    role
    grade
  }
}

## Part 3

query globalSearch {
  search (input: {name: "a"}){
    __typename
    ... on Person {
      name
    }
    ... on Skill {
      name
    }
  }
}

Technologies

Select the exercise on your preferred technology:

Learning resources