Meteor alternative to GraphQL


#1

Updated: renamed DataRouter.routes to DataGraph.relationships for clarity following @SkinnyGeek1010’s comment.

GraphQL and Falcor illustrate intuitive approaches to hierarchical data queries. We can cover some of their features pretty easily with Meteor as it stands. I have included a description of my approach. It requires very little code to implement and works well for an internal app that I am working on. I originally posted this as a comment in an old crater.io post, but I think that this is a better place to discuss.

As an example, let’s use a fictitious blog app. In this app we have a blog post page that needs information about the post that is scattered throughout various collections including the Comments, Blogs, and Authors collections.

Here is a high level description of the data that is needed. We start with a blog post with a given id and extend it with information about its blog, titles of other posts from the same blog, and some information about its comments.

var blogPostTree = postId =>
  include('post', postId,
    include('blog', 
      include('author', nameOnly)
      include('posts', titleOnly)),
    include('comments',
      include('author', nameOnly))); 

var titleOnly = { fields: { title: 1 } };
var nameOnly = { fields: { name: 1 } };

Each document inclusion is backed by a relationship such as the one below, which defines how to get an array of comments from a post. When this relationship is included, the post’s comments field is populated with the array of comments with ids matching the post’s commentIds field, in the order that they appear in the commentIds field.

DataGraph.relationships(Posts, {
  comments: {
    collection: Comments,
    idField: 'commmentIds',
    toMany: true
  }
});

If comment documents held references to posts instead of post documents referencing comments, we would use this relationship description instead.

DataGraph.relationships(Posts, {
  comments: {
    collection: Comments,
    externalField: 'postId',
    toMany: true
  }
});

One DataGraph.relationships invocation can define several data relationships. Here is an example with two relationships.

DataGraph.relationships(Posts, {
  comments: {
    collection: Comments,
    idField: 'commmentIds',
    toMany: true
  },
  blog: {
    collection: Blogs,
    idField: 'blogId',
    toMany: false
  }
});

All that is left to fully specify the meaning of blogPostTree is to define a post entry point to give meaning to the tree root, include(‘post’, postId, …). I have included some rudimentary security as an example.

DataGraph.entryPoints({
  post: {
    type: Posts,
    toMany: false,
    resolve: (postId, publication) =>
      (publication && !publication.userId) ?
        null :
        Posts.find({ _id: postId })
  }
});

Now we can use blogPostTree for fetching the desired document tree and for publishing the required documents from different collections.

// On the server
DataGraph.publishDataGraph('blogPostPage', blogPostTree);

// On the client
Meteor.subscribe('blogPostPage', postId)
...
var tree = taskPageTree(taskId);
var post = DataGraph.fetch(tree);

The post object holds a hierarchy of documents from various collections. It looks a something like this.

var blogPostTree = postId =>
  include('post', postId,
    include('blog', 
      include('author', nameOnly)
      include('posts', titleOnly)),
    include('comments',
      include('author', nameOnly))); 


// blogPostTree('adsf') 
{
  _id: 'adsf'
  title: 'Meteor rocks!',
  blog: {
    _id: '...',
    name: 'Meteor blog',
    author: {
      _id: '...',
      name: 'John Smith',
    },
    posts: [
      { 
        _id: '...', 
        title: 'Meteor for LOGO developers' 
      },
      { 
        _id: '...', 
        title: 'Meteor for COBOL developers' 
      }
    ],
  },
  comments: [
    {
      _id: '...',
      message: 'I could have done better!',
      author: {
        _id: '...',
        name: 'genius1'
      }
    },
    {
      _id: '...',
      message: 'Meh',
      author: {
        _id: '...',
        name: 'genius2'
      }      
    },
  ]
}

I may have some typos, but I hope that the idea comes through. Also, I haven’t tried to implement GraphQL-like fragments, which allow you to merge requirements from an entire component tree into one request.

Please let me know what you think about this approach and how you do things differently.


#2

I have updated the title and made a couple of changes to the post based on feedback.


#3

I think in general an easy to use query system like GraphQL would be ideal!

Would it be possible to use this in each top-level child template or does it have to be specified in the router? My personal opinion is that subscribing to data in the router can get really messy unless it’s just a simple posts subscription with a url param. If you have business logic to determine the subscription it tends to make a ball of mess.

I have also been playing around with fetching by fields and thinking about a Meteor Relay type of system to subscribe to data using the previous fields.

The resulting JSON looked something like this:

fieldsNeeded: {
        posts: {
          _id: true,
          desc: true,
          likeCount: true,
          commentCount: true,
          userName: true,
          createdAt: true,
          ownerId: true,
        },
        postComments: {
          _id: true,
          createdAt: true,
          username: true,
          desc: true,
          postId: true,
        }
      }
    };
  },

Though ideally the children would require what fields they need and then it gets composed before the subscription. Something like this?


#4

Thank you for your feedback, @SkinnyGeek1010, I have renamed DataRouter.routes to DataGraph.relationships to reflect the fact that they represent how to assemble documents from different collections rather than having anything to do with application routing. Relationships (formally routes) are similar to GraphQL schemas.

The key point is that you create an explicit description of your document graph by specifying relationships between documents of various collections, and then fetch document trees from the graph using a simple declarative data representation. You get back a single object containing your full document tree. Meteor makes it easy to update this object reactively. This approach also allows you to subscribe to ad hoc data trees from the client side, while controlling access through data graph relationships.

On a separate note, specifying the data tree using your approach might make things clearer. I could convert my DSL to look more like the simplification below, or the more explicit case that is similar to your example. Note that in my case, relationships can take arguments, so it’s a little different from your case. Alternatively, I could just use GraphQL instead of these DSLs.

// my DSL
var blogPostTree = postId =>
  include('post', postId,
    include('blog', 
      include('author', nameOnly)
      include('posts', titleOnly)),
    include('comments',
      include('author', nameOnly))); 

// simplification
var blogPostTree = postId =>
  ['post', postId,
    ['blog', 
      ['author', nameOnly]
      ['posts', titleOnly]],
    ['comments',
      ['author', nameOnly]]]; 
      
      
// more explicit      
var blogPostTree = postId => ({
  post: [
    postId, 
    {
      title: true,
      blog: {
        title: true,
        author: { name: true},
        posts: {
          title: true
        },
      },
      comments: {
        message: true,
        author: {
          name: true
        }
      }
  }
});

I will read through your work and post here. Perhaps we are following a similar path. Does your approach return one hierarchical object containing data from different collections as above, or do you get separate objects?


#5

It was meant to return once object but the hard part is managing reactivity. I’m starting to think a separate publication for each collection is the way to go. Perhaps the client could run a method to aggregate the two together once both subs have finished (or not for faster loading).

I haven’t had enough free time to get any further than this. One alternative is to just use the GraphQL babel transformer and then use the output to publish somehow.


#6

That makes sense. I approach this by creating an AST that can be run on the client to generate the desired data structure but can also be converted into input for reywood:publish-composite on the server side.

I have briefly explored using GraphQL. The key issue with going down that route is that you have to commit to providing types for all fields, and child objects of documents. I’m not sure if this meshes with the Meteor way of doing things.