Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

many-to-many relationship in GraphQL

Let's say I have two Arrays of Objects:

let movies = [
    { id: '1', title: 'Erin Brockovich'},
    { id: '2', title: 'A Good Year'},
    { id: '3', title: 'A Beautiful Mind'},
    { id: '4', title: 'Gladiator'}
];

let actors = [
    { id: 'a', name: 'Julia Roberts'},
    { id: 'b', name: 'Albert Finney'},
    { id: 'c', name: 'Russell Crowe'}
];

I want to create many-to-many relationship betweet them. For the begining in Vanilla JavaScript , eventually in GraphQL schema.

In JavaScript I did something like this:

let movies = [
    { id: '1', title: 'Erin Brockovich',  actorId: ['a', 'b'] },
    { id: '2', title: 'A Good Year',      actorId: ['b', 'c'] },
    { id: '3', title: 'A Beautiful Mind', actorId: ['c'] },
    { id: '4', title: 'Gladiator',        actorId: ['c'] }
];
let actors = [
    { id: 'a', name: 'Julia Roberts', movieId: ['1'] },
    { id: 'b', name: 'Albert Finney', movieId: ['1', '2'] },
    { id: 'c', name: 'Russell Crowe', movieId: ['2', '3', '4'] }
];
let actorIds = [];
let movieIds = [];
for (let m = 0; m < movies.length; m ++) {
    for (let i = 0; i < movies[m].actorId.length; i ++) {
        actorIds.push(movies[m].actorId[i]);
    }
}
for (let a = 0; a < actors.length; a ++) {
    for (let i = 0; i < actors[a].movieId.length; i ++) {
        movieIds.push(actors[a].movieId[i]);
    }
}
for (let a = 0; a < actors.length; a ++) {
    for (let i = 0; i < actorIds.length; i ++) {
        if ((actors[a].id == 'c') && (actors[a].id == actorIds[i])) {
            for (let j = 0; j < movies.length; j ++) {
                if (movies[j].id == movieIds[i]) {
                    console.log(movies[j].title);
                }
            }
        }
    }
}

When I run preceding code in Node, Terminal will return

A Good Year
A Beautiful Mind
Gladiator

and this is exactly what I want.

Unfortunately I've get lost in GraphQL schema. What I have so far—inside of fields function of course—is this:

in_which_movies: {
    type: new GraphQLList(FilmType),
    resolve(parent, args) {

        let actorIds = [];
        let movieIds = [];

        for (let m = 0; m < movies.length; m ++) {
            for (let i = 0; i < movies[m].actorId.length; i ++) {
                actorIds.push(movies[m].actorId[i]);
            }
        }
        for (let a = 0; a < actors.length; a ++) {
            for (let i = 0; i < actors[a].movieId.length; i ++) {
                movieIds.push(actors[a].movieId[i]);
            }
        }
        for (var a = 0; a < actors.length; a ++) {
            for (var i = 0; i < actorIds.length; i ++) {
                if ((actors[a].id == parent.id) && (actors[a].id == actorIds[i])) {
                    for (var j = 0; j < movies.length; j ++) {
                        if (movies[j].id == movieIds[i]) {
                            console.log(movies[j].title);
                        }
                    }
                }
            }
        }
        return movies[j].title;
    }
}

When I run the following query in GraphiQL...

{
    actor(id: "c") {
        name
        in_which_movies {
            title
        }
    }
}

...I have this response:

{
  "errors": [
    {
      "message": "Cannot read property 'title' of undefined",
      "locations": [
        {
          "line": 4,
          "column": 3
        }
      ],
      "path": [
        "actor",
        "in_which_movies"
      ]
    }
  ],
  "data": {
    "actor": {
      "name": "Russell Crowe",
      "in_which_movies": null
    }
  }
}

...which is strange for me, cuz Terminal is responding as I expected

A Good Year
A Beautiful Mind
Gladiator

I guess all code I've wrote so far is useless and I need some fresh guidelines how to write many-to-many relationship in GraphQL properly.

like image 983
Tylik Stec Avatar asked Apr 18 '18 15:04

Tylik Stec


People also ask

What are the three types of operations in GraphQL?

GraphQL works by sending operations to an endpoint. There are three types of operations: queries, mutations, and subscriptions. A query is sent through an HTTP POST call to retrieve data. A mutation is also sent through a HTTP POST and is used to modify data.

How do you create a nested query in GraphQL?

You can use the object (one-to-one) or array (one-to-many) relationships defined in your schema to make a nested query i.e. fetch data for a type along with data from a nested or related type. The name of the nested object is the same as the name of the object/array relationship configured in the console.

Does GraphQL support double?

GraphQL scalar types Here are the primitive data types that GraphQL supports: Int – A signed 32-bit integer. Float – A signed double precision floating point value.


1 Answers

TL;DR I think you're way overthinking things. Your resolvers are doing way too much work and that's resulting in code that's hard to reason about and hard to debug.

I don't think your question really has much to do with GraphQL but just getting the right operations on your underlying data. I'll try to step through it starting with your example and in terms of GraphQL so we end up with the types and resolvers you're looking for.

Starting with your vanilla code:

let movies = [
    { id: '1', title: 'Erin Brockovich',  actorId: ['a', 'b'] },
    { id: '2', title: 'A Good Year',      actorId: ['b', 'c'] },
    { id: '3', title: 'A Beautiful Mind', actorId: ['c'] },
    { id: '4', title: 'Gladiator',        actorId: ['c'] }
];
let actors = [
    { id: 'a', name: 'Julia Roberts', movieId: ['1'] },
    { id: 'b', name: 'Albert Finney', movieId: ['1', '2'] },
    { id: 'c', name: 'Russell Crowe', movieId: ['2', '3', '4'] }
];

I'd like to suggest that we translate this into something indexed by id so that it's easier to query. That will also better model a database or key-value store that you'll usually have behind a production GraphQL API.

Translating this to something indexed, but still vanilla JS:

let movies = {
    '1': { id: '1', title: 'Erin Brockovich',  actorId: ['a', 'b'] },
    '2': { id: '2', title: 'A Good Year',      actorId: ['b', 'c'] },
    '3': { id: '3', title: 'A Beautiful Mind', actorId: ['c'] },
    '4': { id: '4', title: 'Gladiator',        actorId: ['c'] }
};
let actors = {
    'a': { id: 'a', name: 'Julia Roberts', movieId: ['1'] },
    'b': { id: 'b', name: 'Albert Finney', movieId: ['1', '2'] },
    'c': { id: 'c', name: 'Russell Crowe', movieId: ['2', '3', '4'] }
};

Next, we should think about the GraphQL schema that will represent these types. Here's where the "many to many" part comes into play. I think we can pretty cleanly derive the types from your example data:

type Movie {
  id: ID!
  title: String
  actors: [Actor]
}

type Actor {
  id: ID!
  name: String
  movies: [Movie]
}

Note that [Movie] is a list of Movie objects. Even though the underlying data contains ids (aka "normalized", which is what we would expect) we model the API in terms of the actual typed relationships.

Next we need to set up the resolvers. Let's look at the Actor type's resolvers since that's what is in your example.

movies: {
    type: new GraphQLList(FilmType),
    resolve(parent) {
        // The ids of all the movies this actor is in. "parent" will be the
        // actor data currently being queried
        let movieIds = parent.movieId;
        // We'll build up a list of the actual movie datas to return.
        let actorInMovies = [];
        for (let m = 0; m < movieIds.length; m++) {
            // The m'th movie id.
            let movieId = movieIds[m];
            // The movie data from our indexed "movies" top level object.
            // In production, this might access a database service
            let movie = movies[movieID];
            // Add that movie to the list of movies
            actorInMovies.push(movie)
        }
        // Then we'll return that list of movie objects.
        return actorInMovies;
    }
}

Notice that in your original resolver, you returned movies[j].title which is probably a string, and doesn't match up with what would be expected by "List of FilmType", and in my example above an array of movie data objects is returned.

Also, the code above is a quite verbose way to do this, but I thought it would be helpful to comment on each step. To be truly many-to-many, then the Movie type should have nearly identical code for it's actors field. However just to show an example of how this code can be greatly simplified by using the .map() operator, I'll write it another way:

actors: {
    type: new GraphQLList(ActorType),
    resolve(parent) {
        // In this case, "parent" will be the movie data currently being queried.
        // Use "map" to convert a list of actor ids into a list of actor data 
        // objects using the indexed "actors" top level object.
        return parent.actorId.map(id => actors[id]);
    }
}
like image 170
Lee Byron Avatar answered Oct 20 '22 12:10

Lee Byron