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:
-
Finds any
Actor
nodes with the name "Woody Harrelson". -
Connects the "Woody Harrelson" node to a
Production
node with the title "Zombieland". -
Connects the connected
Production
node to anyActor
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
}
}
}
}