Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Access parent data from child resolver #52

Closed
HriBB opened this issue May 13, 2016 · 10 comments
Closed

Access parent data from child resolver #52

HriBB opened this issue May 13, 2016 · 10 comments

Comments

@HriBB
Copy link

HriBB commented May 13, 2016

Hello

I am trying out Apollostack and so far it's been great! I have a problem though :)

This question might be better asked on sequelize forums, but maybe there's some apollo feature I am not aware of ...

I have a sequelize model with parent->children relation on the same table, and I need to access parent data from inside the child's resolve functions or from inside sequelize's instance methods.

This is my sequelize model Location

module.exports = function(sequelize, DataTypes) {
  return sequelize.define('Location', {
    parent_id: {
      type: DataTypes.INTEGER(11),
      allowNull: true,
    },
    name: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    slug: {
      type: DataTypes.STRING,
      allowNull: false,
    }
  },{
    tableName: 'location',
    freezeTableName: true,
    instanceMethods: {
      getUrl: function() {
        // here I need to check if this instance is a child
        // and return a different url for child
        return '';
      }
    },
    classMethods: {
      associate: function(m) {
        m.Location.belongsTo(m.Location, {
          foreignKey: 'parent_id',
          as: 'Parent'
        });
        m.Location.hasMany(m.Location, {
          foreignKey: 'parent_id',
          as: 'Locations'
        });
      }
    }
  });
};

And this are my resolve functions

const resolveFunctions = {
  RootQuery: {
    location(root, { slug }, context){
      return Location.find({ where: { slug }, include:[{ model: Location, as: 'Locations' }] });
    }
  },
  Location: {
    parent(location){
      return location.getParent();
    },
    locations(location){
      return location.getLocations();
    },
    url(location){
      // or here ... 
      // check if this location is child
      // and return a different url
      return location.getUrl();
    }
  }
}

What would be the best way to do this?

This is the solution I have come up with ... I manually inject parent data into child.

const resolveFunctions = {
  RootQuery: {
    // ...
  },
  Location: {
    locations(location){
      if (!location.Locations) {
        return [];
      }
      // I can manually "inject" parent into each location
      // this way I can access this.parent from within getUrl() inside instanceMethods
      return location.Locations.map(l => {
        l.parent = location.dataValues;
        return l;
      });
    }
  }
}
@helfer
Copy link
Contributor

helfer commented May 13, 2016

I don't know much about sequelize, but in GraphQL the way to do this would be to pass the parent to the child by including it in the resolved value. Something like this:

Parent: {
  children(parent){
    return parent.children.map( child => [parent, child] ); // or { parent, child }
  },
}

@HriBB
Copy link
Author

HriBB commented May 31, 2016

Thanks @helfer

@HriBB HriBB closed this as completed May 31, 2016
@HriBB
Copy link
Author

HriBB commented Jun 2, 2016

@helfer so I tried your suggestion, but it did not work as expected. Children resolver is expected to return an array of Location objects, not an array of [parent, child]˙ arrays or{parent, child}` objects.

I did manage to get it working with context though ...

The resolve function's optional third argument is a context value that
is provided to every resolve function within an execution. It is commonly
used to represent an authenticated user, or request-specific caches.

Here's my solution in case someone else has similar question.

const resolveFunctions = {
  RootQuery: {
    location(root, { slug }, context, options){
      context.locations = {}
      context.routes = {}
      context.files = {}
      return Location.find({ where: { slug }}).then(location => {
        // store location references into context, mapped by id
        context.locations[location.id] = location
        return location
      })
    },
  },
  Location: {
    locations(location, args, context){
      return location.getLocations().then(locations => {
        // store location children into context, mapped by id
        locations.forEach(location => context.locations[location.id] = location)
        return locations
      })
    },
    files(location, args, context){
      return location.getFiles().then(files => {
        // store file references into context, mapped by id
        files.forEach(files => context.files[files.id] = files)
        return files
      })
    },
    routes(location, args, context){
      return location.getRoutes()
    },
    url(location, args, context){
      // access data from context
      return location.parent_id
        ? getLocationUrl(context.locations[location.parent_id], location)
        : getLocationUrl(location)
    }
  },
  File: {
    url(file, args, context){
      // access data from context
      const loc = context.locations[file.location_id]
      const location = loc.parent_id ? context.locations[loc.parent_id] : loc
      const sector = loc.parent_id ? loc : null
      return getLocationUrl(location, sector, file)
    }
  },
  Route: {
    url(route, args, context) {
      // access data from context
      const loc = context.locations[route.location_id]
      const location = loc.parent_id ? context.locations[loc.parent_id] : loc
      const sector = loc.parent_id ? loc : null
      const file = context.files[route.file_id]
      return getLocationUrl(location, sector, file, route)
    }
  }
}

Is this OK, or is there a better way of doing it?

BTW is this related to the Loader/Connector API I've been reading about in #21?

@helfer
Copy link
Contributor

helfer commented Jun 2, 2016

@HriBB: Passing it through the context is probably not a great solution, because the context is shared by all resolvers, and you have no control over the order in which they run.

Reading this again, I'm not sure why passing the location object is not enough? Aren't you just calling a function on the location? If you pass the sequelize model of that location, it should just work.


@HriBB
Copy link
Author

HriBB commented Jun 2, 2016

@helfer yeah I figured that the order of resolvers would be the problem with the context.

I can attach parent to each child like this

const resolveFunctions = {
  RootQuery: {
    location(root, { slug }, context, options){
      return Location.find({ where: { slug }})
    }
  },
  Location: {
    locations(parent, args, context){
      // attach parent to each location
      return parent.getLocations()
        .then(locations => locations.map(location => Object.assign(location, { parent })))
    },
    url(location, args, context){
      // use the attached parent
      return location.parent
        ? getLocationUrl(location.parent, location)
        : getLocationUrl(location)
    }
  }
}

It would be cool if things like this would be explained in the docs. Are there any examples?

@helfer
Copy link
Contributor

helfer commented Jun 2, 2016

@HriBB I think this is basically how resolvers work. Maybe we can add a paragraph that explains that? I wrote a medium post about a week ago that could be used as a starting point: https://medium.com/apollo-stack/graphql-explained-5844742f195e

@HriBB
Copy link
Author

HriBB commented Jun 3, 2016

@helfer great reading, we need more articles like this.

Note: This example was slightly simplified. A real production GraphQL server would use batching and caching to reduce the number of requests to the backend and avoid making redundant ones — like fetching the same author twice. But that’s a topic for another post!

This is exactly what I need! Can't wait for another post :) And just to clarify the above quote ... by backend you mean database GraphQL server?

I would love to see some more complex examples on this topic. I can help with that, but first I need to understand how things work. Problem with apollo-server is that it is so f***ing simple to set up, that I skipped the reading part and went straight to the implementation and now I wanna do complex stuff without understanding the basics :P Need to do some research ... Can you recommend some good articles?

Let's say I have a simple GraphQL query like this:

{
  posts {
    id
    title
    author {
      name
    }
  }
}

First I fetch all posts, then for each post fetch the author. So if I have 100 posts, obviously I don't want to run 100 queries for each post author, I wanna batch them together, either with a INNER JOIN users or with WHERE post_id IN(post_ids). But where do we store post_ids for example? In context? Should we even do this? Are there any official recommendations?

Furthermore, what if some resolvers "depend" on other resolvers? I know that this is not possible, but in my case, I need it. I have a hierarchy like this parent > child > image > route and the urls look like this

parent
http://domain.com/location/parent-slug

child
http://domain.com/location/parent-slug/child-slug

image
http://domain.com/location/parent-slug/child-slug/image-slug

route
http://domain.com/location/parent-slug/child-slug/image-slug/route-slug

I want to be able to generate urls on the server, and this is where things get complicated. When I try to resolve the image url for example, I need all the players: getRouteUrl(parent, child, image, route). I could fetch all from db, but then I would end up with hundreds of queries. Not really sure how to approach this ... Are there any good docs/examples?§ In traditional REST API this is easy, because I can fetch the right data at the right time, and easily pass it around to functions when needed ...

Anyway, thanks for your help @helfer. I will stop bugging you now, as I know you have better work to do ;)

@HriBB
Copy link
Author

HriBB commented Jun 3, 2016

This issue offers some good reading

@HriBB
Copy link
Author

HriBB commented Jun 3, 2016

And there's the DataLoader :)
Just for the reference, in case someone else reads this topic

@helfer
Copy link
Contributor

helfer commented Jun 3, 2016

@HriBB Unfortunately there's not that much good advanced reading material out there for GraphQL yet. We're working on that.

The thing dataloader helps you do is not create queries with JOIN in them, but to batch togeter all requests at the same level. So say you want to get a list of N posts and info about all their authors. Potentially you could be doing N + 1 queries. With a JOIN you could do one (bigger and slower) query. With proper batching, you'll be doing two queries. One for the post and one for the N authors. Depending on what the latency of your database is, the 2 queries can actually be more efficient than the large JOIN query. It's hard to know for sure though and depends on the situation, which is why we're building a tool called tracer that will collect data and help people figure out how to make their server more efficient.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants