0

This is closely related to my last question here. In short, I have 2 schemas, dbPosts and dbAuthors. They look somewhat like this (I've omitted some fields here for the sake of brevity):

dbPosts

id: mongoose.Schema.Types.ObjectId,
title: { type: String },
content: { type: String },
excerpt: { type: String },
slug: { type: String },
author: {
   id: { type: String },
   fname: { type: String },
   lname: { type: String },
}

dbAuthors

id: mongoose.Schema.Types.ObjectId,
fname: { type: String },
lname: { type: String },
posts: [
         id: { type: String },
         title: { type: String }
       ]

I'm resolving my post queries like this:

const mongoose = require('mongoose');
const graphqlFields = require('graphql-fields');
const fawn = require('fawn');
const dbPost = require('../../../models/dbPost');
const dbUser = require('../../../models/dbUser');

fawn.init(mongoose);

module.exports = {
  // Queries
  Query: {
    posts: (root, args, context) => {
      return dbPost.find({});
    },
    post: (root, args, context) => {
      return dbPost.findById(args.id);
    },
  },

  Post: {
    author: (parent, args, context, ast) => {
      // Retrieve fields being queried
      const queriedFields = Object.keys(graphqlFields(ast));
      console.log('-------------------------------------------------------------');
      console.log('from Post:author resolver');
      console.log('queriedFields', queriedFields);
      // Retrieve fields returned by parent, if any
      const fieldsInParent = Object.keys(parent.author);
      console.log('fieldsInParent', fieldsInParent);
      // Check if queried fields already exist in parent
      const available = queriedFields.every((field) => fieldsInParent.includes(field));
      console.log('available', available);
      if(parent.author && available) {
        return parent.author;
      } else {
        return dbUser.findOne({'posts.id': parent.id});
      }
    },
  },
};

And I'm resolving all author queries like this:

const mongoose = require('mongoose');
const graphqlFields = require('graphql-fields');
const dbUser = require('../../../models/dbUser');
const dbPost = require('../../../models/dbPost');

module.exports = {
  // Queries
  Query: {
    authors: (parent, root, args, context) => {
      return dbUser.find({});
    },
    author: (root, args, context) => {
      return dbUser.findById(args.id);
    },
  },

  Author: {
    posts: (parent, args, context, ast) => {
      // Retrieve fields being queried
      const queriedFields = Object.keys(graphqlFields(ast));
      console.log('-------------------------------------------------------------');
      console.log('from Author:posts resolver');
      console.log('queriedFields', queriedFields);
      // Retrieve fields returned by parent, if any
      const fieldsInParent = Object.keys(parent.posts[0]._doc);
      console.log('fieldsInParent', fieldsInParent);
      // Check if queried fields already exist in parent
      const available = queriedFields.every((field) => fieldsInParent.includes(field));
      console.log('available', available);
      if(parent.posts && available) {
        // If parent data is available and includes queried fields, no need to query db
        return parent.posts;
      } else {
        // Otherwise, query db and retrieve data
        return dbPost.find({'author.id': parent.id, 'published': true});
      }
    },
  },
};

Again, I've left out bits not relevant to this question, such as mutations, in the interest of brevity. My objective is to make all queries work recursively while also optimizing database lookups. But somehow I'm unable to accomplish this. Here's one query I'm running, for instance:

{
  posts{
    id
    title
    author{
      first_name
      last_name
      id
      posts{
        id
        title
      }
    }
  }
}

And it returns this:

{
  "errors": [
    {
      "message": "Cannot return null for non-nullable field Post.author.",
      "locations": [
        {
          "line": 5,
          "column": 5
        }
      ],
      "path": [
        "posts",
        1,
        "author"
      ]
    }
  ],
  "data": {
    "posts": [
      {
        "id": "5ba1f3e7cc546723422e62a4",
        "title": "A Title!",
        "author": {
          "first_name": "Bill",
          "last_name": "Erby",
          "id": "5ba130271c9d440000ac8fc4",
          "posts": [
            {
              "id": "5ba1f3e7cc546723422e62a4",
              "title": "A Title!"
            }
          ]
        }
      },
      null
    ]
  }
}

If you notice, this query does return all values requested, but also adds an error message against the post.author query! What could be causing this?

I haven't included the entire codebase so as not to make things confusing, but should you wish to take a look, it's up on Github and a GraphiQL interface is up at https://graph.schandillia.com should you wish to see the results for yourself.

Thank you so much for your time, if you've come this far. Would really appreciate any pointer in the right direction!"

P.S.: If you notice, I'm logging the values of 3 variables in each resolver for debugging purposes:

  1. queriedFields: An array of all fields being queried
  2. fieldsInParent: An array of all fields being returned in the resolver's parent property
  3. available: A boolean showing if all queriedFields members exist in fieldsInParent

And when I run a simple query like this:

{
  posts{
    id
    author{
      id
      posts{
        id
      }
    }
  }
}

This is what gets logged:

-------------------------------------------------------------
from Post:author resolver
queriedFields [ 'id', 'posts' ]
fieldsInParent [ '$init', 'id', 'first_name', 'last_name' ]
available false
-------------------------------------------------------------
from Post:author resolver
queriedFields [ 'id', 'posts' ]
fieldsInParent [ '$init', 'id', 'first_name', 'last_name' ]
available false
-------------------------------------------------------------
from Author:posts resolver
queriedFields [ 'id' ]
fieldsInParent [ 'id', 'title' ]
available true

Shouldn't the post:author resolver execute only once? Also, it's funny how in the first 2 logs, fieldsInParent is missing the posts field even when the schema for author includes such a field.

TheLearner
  • 2,813
  • 5
  • 46
  • 94

1 Answers1

1

Your query result does not in fact include all the requested data. The posts query resolves to an array that includes one Post object and a null. The null is there because GraphQL tried to fully resolve the other Post object and could not -- it encountered a validation error, namely that the post's author resolved to null.

You can change your schema to make the author field nullable, which would get rid of the error but would still leave you with the null post. Presumably, if a post exists, it should have an author (although with MongoDB I guess it's very possible you just have some bad data). If you look inside your resolver, there's two return statements -- one of them (probably the db call) is returning null for that second post.

As an aside, as a client, you probably don't want to deal with nulls inside the array and want an empty array instead of a null for the whole field. When using lists (arrays), you may want to make them both non-nullable and make each item in that list non-nullable as well. You do so like this:

posts: [Post!]!

You still need to ensure your resolver logic prevents those nulls from happening, but adding the validation can help you catch that sort of behavior more easily.

Daniel Rearden
  • 80,636
  • 11
  • 185
  • 183