Interface types

This page describes how to use and define interfaces on relationship fields.

Creating an interface field

The following schema defines a Actor type, that has a relationship ACTED_IN, of type [Production!]!. Production is an interface type with Movie and Series implementations. Note in this example that relationship properties have also been used with the @relationshipProperties directive, so that interfaces representing relationship properties can be easily distinguished.

interface Production {
    title: String!
    actors: [Actor!]!
}

type Movie implements Production {
    title: String!
    actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn")
    runtime: Int!
}

type Series implements Production {
    title: String!
    actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn")
    episodes: Int!
}

interface ActedIn @relationshipProperties {
    role: String!
}

type Actor {
    name: String!
    actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn")
}

These type definitions will be used for the rest of the examples in this chapter.

Directive inheritance

Any directives present on an interface or its fields will be "inherited" by any object types implementing it. For example, the type definitions above could be refactored to have the @relationship directive on the actors field in the Production interface instead of on each implementing type as it is currently:

interface Production {
    title: String!
    actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn")
}

type Movie implements Production {
    title: String!
    actors: [Actor!]!
    runtime: Int!
}

type Series implements Production {
    title: String!
    actors: [Actor!]!
    episodes: Int!
}

interface ActedIn @relationshipProperties {
    role: String!
}

type Actor {
    name: String!
    actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn")
}

Querying an interface

Which implementations are returned by a query are dictated by the where filter applied.

For example, the following will return all productions with title starting "The " for every actor:

query GetProductionsStartingWithThe {
    actors {
        name
        actedIn(where: { node: { title_STARTS_WITH: "The " } }) {
            title
            ... on Movie {
                runtime
            }
            ... on Series {
                episodes
            }
        }
    }
}

Whilst the query below will only return the movies with title starting with "The " for each actor.

query GetMoviesStartingWithThe {
    actors {
        name
        actedIn(where: { node: { _on: { Movie: { title_STARTS_WITH: "The " } } } }) {
            title
            ... on Movie {
                runtime
            }
        }
    }
}

This is to prevent overfetching, and you can find an explanation of this here.

Alternatively, these implementation specific filters can be used to override filtering for a specific implementation. For example, if you wanted all productions with title starting with "The ", but movies with title starting with "A ", you could achieve this using the following:

query GetProductionsStartingWith {
    actors {
        name
        actedIn(where: { node: { title_STARTS_WITH: "The ", _on: { Movie: { title_STARTS_WITH: "A " } } } }) {
            title
            ... on Movie {
                runtime
            }
            ... on Series {
                episodes
            }
        }
    }
}

Creating using an interface field

The below mutation creates an actor and some productions they’ve acted in:

mutation CreateActorAndProductions {
    createActors(
        input: [
            {
                name: "Chris Pratt"
                actedIn: {
                    create: [
                        {
                            edge: {
                                role: "Mario"
                            }
                            node: {
                                Movie: {
                                    title: "Super Mario Bros"
                                    runtime: 90
                                }
                            }
                        }
                        {
                            edge: {
                                role: "Starlord"
                            }
                            node: {
                                Movie: {
                                    title: "Guardians of the Galaxy"
                                    runtime: 122
                                }
                            }
                        }
                        {
                            edge: {
                                role: "Andy"
                            }
                            node: {
                                Series: {
                                    title: "Parks and Recreation"
                                    episodes: 126
                                }
                            }
                        }
                    ]
                }
            }
        ]
    ) {
        actors {
            name
            actedIn {
                title
            }
        }
    }
}

Nested interface operations

Operations on interfaces are abstract until you instruct them not to be. Take the following example:

mutation CreateActorAndProductions {
    updateActors(
        where: { name: "Woody Harrelson" }
        connect: {
            actedIn: {
                where: { node: { title: "Zombieland" } }
                connect: { actors: { where: { node: { name: "Emma Stone" } } } }
            }
        }
    ) {
        actors {
            name
            actedIn {
                title
            }
        }
    }
}

The above mutation:

  1. Finds any Actor nodes with the name "Woody Harrelson".

  2. Connects the "Woody Harrelson" node to a Production node with the title "Zombieland".

  3. Connects the connected Production node to any Actor nodes with the name "Emma Stone".

This query, however, is fully abstract. If you want to only connect Movie nodes to Actor nodes with name "Emma Stone", you could instead do:

mutation CreateActorAndProductions {
    updateActors(
        where: { name: "Woody Harrelson" }
        connect: {
            actedIn: {
                where: { node: { title: "Zombieland" } }
                connect: { _on: { Movie: { actors: { where: { node: { name: "Emma Stone" } } } } } }
            }
        }
    ) {
        actors {
            name
            actedIn {
                title
            }
        }
    }
}

Alternatively, you can also make sure you only connect to Movie nodes with title "Zombieland":

mutation CreateActorAndProductions {
    updateActors(
        where: { name: "Woody Harrelson" }
        connect: {
            actedIn: {
                where: { node: { _on: { Movie: { title: "Zombieland" } } } }
                connect: { actors: { where: { node: { name: "Emma Stone" } } } }
            }
        }
    ) {
        actors {
            name
            actedIn {
                title
            }
        }
    }
}

Directive inheritance

For the next example, consider the following schema. It defines an Actor type, that has a relationship ACTED_IN, of type [Production!]!. Production is an interface type with Movie and Series implementations. In this example, relationship properties have also been used with the @relationshipProperties directive, so that interfaces representing relationship properties can be easily distinguished:

interface Production {
    title: String!
    actors: [Actor!]!
}

type Movie implements Production {
    title: String!
    actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn")
    runtime: Int!
}

type Series implements Production {
    title: String!
    actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn")
    episodes: Int!
}

interface ActedIn @relationshipProperties {
    role: String!
}

type Actor {
    name: String!
    actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn")
}

Now, considering that any directives present on an interface or its fields are "inherited" by any object types implementing it, the example schema could be refactored to have the @relationship directive on the actors field in the Production interface instead of on each implementing type as it is currently. That is how it would look like:

interface Production {
    title: String!
    actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn")
}

type Movie implements Production {
    title: String!
    actors: [Actor!]!
    runtime: Int!
}

type Series implements Production {
    title: String!
    actors: [Actor!]!
    episodes: Int!
}

interface ActedIn @relationshipProperties {
    role: String!
}

type Actor {
    name: String!
    actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn")
}

Overriding

In addition to inheritance, directives can be overridden on a per-implementation basis. Say you had an interface defining some Content, with some basic authorization rules, such as:

interface Content
    @auth(rules: [{ operations: [CREATE, UPDATE, DELETE], allow: { author: { username: "$jwt.sub" } } }]) {
    title: String!
    author: [Author!]! @relationship(type: "HAS_CONTENT", direction: IN)
}

type User {
    username: String!
    content: [Content!]! @relationship(type: "HAS_CONTENT", direction: OUT)
}

You might implement this once for public content and once for private content which has additional rules in place:

type PublicContent implements Content {
    title: String!
    author: [Author!]!
}

type PrivateContent implements Content
    @auth(rules: [{ operations: [CREATE, READ, UPDATE, DELETE], allow: { author: { username: "$jwt.sub" } } }]) {
    title: String!
    author: [Author!]!
}

The PublicContent type inherits the auth rules from the Content interface, while the PrivateContent type uses the auth rules specified there.

In summary, there are three choices for the application of directives when using interfaces:

  • Directives specified on the interface and inherited by all implementing types when the directives for every type are the same.

  • Directives specified on the interface and overridden by certain implementing types when directives are broadly the same with a few discrepancies.

  • Directives specified on implementing types alone when there is very little commonality between types, or certain types need a directive and others don’t.

Querying an interface

In order to set which implementations are returned by a query, a filter where needs to be applied. For example, the following query returns all productions (movies and series) with title starting "The " for every actor:

query GetProductionsStartingWithThe {
    actors {
        name
        actedIn(where: { node: { title_STARTS_WITH: "The " } }) {
            title
            ... on Movie {
                runtime
            }
            ... on Series {
                episodes
            }
        }
    }
}

This query, on the other hand, only returns the movies with title starting with "The" for each actor:

query GetMoviesStartingWithThe {
    actors {
        name
        actedIn(where: { node: { _on: { Movie: { title_STARTS_WITH: "The " } } } }) {
            title
            ... on Movie {
                runtime
            }
        }
    }
}

This approach aims to prevent overfetching. For more information, read the page Troubleshooting → Preventing overfetching.

Alternatively, these specific filters can also be used to override filtering for a specific implementation. For example, if you want to fetch all series with title starting with "The " and movies with title starting with "A ", you can do it like that:

query GetProductionsStartingWith {
    actors {
        name
        actedIn(where: { node: { title_STARTS_WITH: "The ", _on: { Movie: { title_STARTS_WITH: "A " } } } }) {
            title
            ... on Movie {
                runtime
            }
            ... on Series {
                episodes
            }
        }
    }
}