Search code examples
relayjsgraphql-js

Connection error with Relay


// didn't add node definitions because this problem occurs on initial data fetch

// user type
const userType = new GraphQLObjectType({
  name: 'User',
  fields: () => ({
      id: globalIdField('User'),
      email: { type: GraphQLString },
      posts: {
        type: postConnection,
        args: connectionArgs,
        // getUserPosts() function is below
        resolve: (user, args) => connectionFromArray(getUserPosts(user.id), args),
      },
  }),
  interfaces: [nodeInterface],
})

// post type
const postType = new GraphQLObjectType({
  name: 'Post',
  fields: () => ({
    id: globalIdField('Post'),
    title: { type: GraphQLString },
    content: { type: GraphQLString },
  }),
  interfaces: [nodeInterface],
})  

// connection type
const {connectionType: postConnection} =
  connectionDefinitions({name: 'Post', nodeType: postType})

// Mongoose query on other file
exports.getUserPosts = (userid) => {
  return new Promise((resolve, reject) => {
    Post.find({'author': userid}).exec((err, res) => {
      err ? reject(err) : resolve(res)
    })
  })
}

I get the following warning in browser console:

Server request for query App failed for the following reasons:

  1. Cannot read property 'length' of undefined

    _posts40BFVD:posts(first:10) {

    ^^^


That's the only information I got, there's no more errors or references. What could be the reason?

This code is from relay-starter-kit, I only replaced all the Widget code with Post. Everything is almost the same as in starter, therefore I think the cause is somewhere around the database code.

But I can't see the problem because getUserPosts() returns same structure: array of objects..


What was the problem?

resolve: (user, args) => connectionFromArray(getUserPosts(user.id), args)

getUserPosts() returned a promise (blocking code would probably be a bad idea) but there was no callback. What happened in my opinion was that connectionFromArray() continued executing the code but it didn't have the data from getUserPosts() yet, which caused the whole system to fail.

One possible solution

I use Babel and JavaScript "future features" anyway, therefore I decided to use async and await. But there still was a problem and getUserPosts() returned an empty array.

Then I discovered that if another function is called with await in a async function, that another function has to be async as well, otherwise all await-s fail. Here's my final solution that works:

// async function that makes db query
exports.getUserPosts = async (userId) => {
    try {
      // db query
      const posts = await Post.find({author: args}).exec()
      return posts
    } catch (err) {
      return err
    }
}

// and resolve method in Schema, also async
resolve: async (user, args) => {
  const posts = await getUserPosts(user._id)
  return connectionFromArray(posts, args)
}

Im not still sure though if it's the best way. My logic tells me that I should use async as much as possible in Node but Im far from Node expert. I'll update the question when I know more about it.

I would be glad to know if there's a better or even a recommended way to deal with this database query situation using Relay.


Solution

  • graphql-relay provides a connectionFromPromisedArray function which waits until the promise resolves. resolve: (user, args) => connectionFromPromisedArray(getUserPosts(user.id), args), which probably is the most recommended/easiest way when dealing with promised connections.

    If your user.id is a global ID field then you have to extract the real DB ID from the base64 string const { type, id } = fromGlobalId(user.id);.
    or
    resolve: (user, args) => connectionFromPromisedArray(getUserPosts(fromGlobalId(user.id).id), args),

    In a app I work on which is using Relay and Mongoose I found it better to write my own connection implementation to handle pagination and filtering on the DB level rather than on the app level. I took a lot of inspiration from graffiti-mongoose when I wrote it.