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.