Approach for JSONAPIAdapter with MongoDB nested children


#1

I am developing a frontend in EmberJS, using Ember Data’s JSONAPIAdapter to fetch data from a Go(lang) backend using google’s jsonapi package. I am very happy so far, but ran in to an issue with nesting, which seems to raise much discussion here. Many suggest that collapsing the routes is the solution, so in my case /comic/:comicId/:chapterId (when no chapterId is appended, comic model information is shown) would become /comic/:comicId + /chapter/:chapterId. I would’ve been glad to do this, however the data is stored in our database in a way that makes this impossible: chapters have no unique identifiers, the id is the chapter number (and all comics have e.g. chapter 1, therefore collision between chapters would ensue if /chapter/:chapterId were called). Why? Because comics are stored in the database in such a way (very simplified example):

{
    "_id" : ObjectId("59367bfe9cba950f3628ac91"),
    "title" : "A comic",
    "stub" : "a_comic",
    "authors" : [ 
        "authorname",
        "another author"
    ],
    "artists" : [ 
        "artistname"
    ],
    "genres" : [ 
        "action", 
        "adventure", 
        "fantasy",
        "romance"
    ],
    "rank" : 7,
    "rating" : 4.82,
    "description" : "lorem ipsonium",
    "chapters" : [
        {
            "number" : 3.0,
            "volume" : 0,
            "name" : "",
            "release" : ISODate("2014-08-22T09:54:54.654Z"),
            "pages" : [ 
                {
                    "number" : 1,
                    "filename" : "s001.jpg"
                }, 
                {
                    "number" : 2,
                    "filename" : "s002.jpg"
                }, 
                {
                    "number" : 3,
                    "filename" : "s003.jpg"
                }
            ],
            "status" : 0
        }, 
        {
            "number" : 2.0,
            "volume" : 0,
            "name" : "another name",
            "release" : ISODate("2014-08-09T09:54:55.660Z"),
            "pages" : [ 
                {
                    "number" : 1,
                    "filename" : "i001.jpg"
                }, 
                {
                    "number" : 2,
                    "filename" : "i002.jpg"
                }, 
                {
                    "number" : 3,
                    "filename" : "i003.jpg"
                }
            ],
            "status" : 0
        }, 
        {
            "number" : 1.0,
            "volume" : 0,
            "name" : "first chapter",
            "release" : ISODate("2014-07-26T09:54:56.674Z"),
            "pages" : [ 
                {
                    "number" : 1,
                    "filename" : "b001.jpg"
                }, 
                {
                    "number" : 2,
                    "filename" : "b002.jpg"
                }
            ],
            "status" : 0
        }
    ]
}

As you can see, the chapters are just an array nested in the comic, so I have no way of querying the database (mongodb) for an individual chapter without knowing which comic it is from.

Output from the golang API that ember relies on looks like this:

{
    "data": {
        "type": "comics",
        "id": "59367bfe9cba950f3628ac91",
        "attributes": {
            "artists": ["authorname", "another author"],
            "authors": ["artistname"],
            "description": "lorem ipsonium",
            "genres": ["action", "adventure", "fantasy", "romance"],
            "rank": 7,
            "rating": 482,
            "stub": "a_comic",
            "title": "A comic",
        },
        "relationships": {
            "chapters": {
                "data": [{
                    "type": "chapters",
                    "id": "1"
                }, {
                    "type": "chapters",
                    "id": "2"
                }, {
                    "type": "chapters",
                    "id": "3"
                }]
            }
        }
    },
    "included": [{
        "type": "chapters",
        "id": "1",
        "attributes": {
            "name": "first chapter",
            "release": "2015-12-09T01:54:33-08:00",
            "volume": 0
        }
    }, {
        "type": "chapters",
        "id": "2",
        "attributes": {
            "name": "another name",
            "release": "2015-11-10T01:54:34-08:00",
            "volume": 0
        }
    }, {
        "type": "chapters",
        "id": "3",
        "attributes": {
            "release": "2016-01-01T01:54:37-08:00",
            "volume": 0
        }
    }]
}

I am able to work with this data just fine, displaying a comic’s details on its page at /comic/:comicId (comicId being the mongodb bson id. Maybe in the future it will be the stub instead, but that is well-documented and not important right now). How, though, should I tell EmberJS that the chapter model’s endpoint is /comic/:comicId/:chapterId in the frontend’s url and /comics/:comicId/:chapterId on the backend as well? I do not include the pages in the comic’s details, because the output would be way too large for a simple info request (chapters have ~24pgs each, and some comics have ~700 chapters). The golang API’s output for /comics/59367bfe9cba950f3628ac91/1 would look like this:

{
    "data": {
        "type": "chapters",
        "id": "1",
        "attributes": {
            "name": "first chapter",
            "release": "2014-07-26T02:54:56-07:00",
            "volume": 0
        },
        "relationships": {
            "pages": {
                "data": [{
                    "type": "pages",
                    "id": "1"
                }, {
                    "type": "pages",
                    "id": "2"
                }]
            }
        }
    },
    "included": [{
        "type": "pages",
        "id": "1",
        "attributes": {
            "filename": "b016.jpg"
        }
    }, {
        "type": "pages",
        "id": "2",
        "attributes": {
            "filename": "b023.jpg"
        }
    }]
}

The only way I found to do this is by using the ember-data-url-templates addon, which just doesn’t seem right at all, because I have to have my chapter adapter be this:

import Ember from "ember";
import DS from 'ember-data';
import ApplicationAdapter from './application';
import UrlTemplates from "ember-data-url-templates";

export default ApplicationAdapter.extend(UrlTemplates, {
    urlTemplate: '{+host}/{+namespace}/comics/{comic}{/id}',
    queryRecordUrlTemplate: '{+host}/{+namespace}/comics/{cid}{/chapter}',
    urlSegments: {
        comic: function(type, id, snapshot, query) {
            return snapshot.belongsTo('comic', {id: true});
        }
    }
});

and route js be this:

import Ember from 'ember';
export default Ember.Route.extend({
  model(params) {
    let { chapter_id } = this.paramsFor('chapter');
    let { comic_id } = this.paramsFor('comic');
    return this.store.queryRecord('chapter', { cid: comic_id, chapter: chapter_id });
  }
});

which results in more than the necessary amount of queries increasing the load on the API. Meanwhile, the comic info page works so much better and only requires that I do:

import Ember from 'ember';
export default Ember.Route.extend({
  model(params) {
    let { comic_id } = this.paramsFor('comic');
    return this.store.findRecord('comic', comic_id);
  }
});

Thus my question: what would be the best approach to getting individual chapter pages to function well? I am allowed to modify the API and frontend freely, just not the database.