ethanresnick / json-api

Turn your node app into a JSON API server (http://jsonapi.org/)
GNU Lesser General Public License v3.0
268 stars 41 forks source link

Is it possible to have a one to many relationship without storing array of child references? #95

Closed shannon closed 9 years ago

shannon commented 9 years ago

I'm trying to set up a basic one to many relationship between Groups and Projects.

Like this:

const groupSchema = new Schema({
  name: String
});

const projectSchema = new Schema({
  group: { type: ObjectId, ref: 'Group' },
  name: String
});

The problem is, I don't think the relationship works correctly unless I add in an array of children to the group schema like so:

const groupSchema = new Schema({
  name: String,
  projects: [{ type: ObjectId, ref: 'Project' }]
});

If I don't add this when I call /groups/{id}/relationships/projects I get a 404 Invalid relationship name message.

But then if I do add it, when I create a new project via POST /projects (specifying the group relationship correctly, I double checked) it doesn't add it to that array so then /groups/{id}/relationships/projects is always empty.

Is there something I am missing? I'm new to jsonapi so I may just be looking at this from the wrong angle.

ethanresnick commented 9 years ago

When you add a project and set its group, you're creating a new document for the project and storing the id of the related group directly on that document. Doing that doesn't modify the document for the group at all, which is why Group.projects always stays empty. This is just a function of how mongo works than anything in the JSON API spec.

If you want creating a project to also update the document for the related group, you need to use a mongoose middleware to do so. And that will give you the ids on both sides exactly as you want.

However, I should say that it's often more trouble than it's worth (perhaps in mongoose in particular) to keep a relationship in sync on both sides. For example, you'll actually need one middleware on each model if you want the relationship to be updatable from both sides. And you may need both query middleware and document middleware, depending on your app. Therefore, you might want to think about storing the group just on the Project schema. Then, when you need all the projects for a group, you query all projects where group == desired group. That's a query that your clients could make (with a separate API request) or its one you could make for them if (in the beforeRender for group) you're willing to have the relationship only be updatable on the project side.

shannon commented 9 years ago

Thanks @ethanresnick

Yea that's kind of what I figured. I don't really want the double link. However what I'm trying to wrap my head around is how to make the relationship discoverable through the groups resource. Trying to follow the HATEOAS principle, which I'm also new to. If I don't configure it with child array then the group resource has no evidence that there is even a project resource to query. Even if there was a way to have a relationship url that just has /projects?filter[simple][group]={id} I would be fine with that. I'm not sure how to do that with json-api though.

shannon commented 9 years ago

Sorry I guess what I'm asking for is, is there a way to define a relationship that isn't based on the mongoose model? I want to explicitly say groups have a one to many relationship with projects.

ethanresnick commented 9 years ago

Trying to follow the HATEOAS principe, which I'm also new to. If I don't configure it with child array then the group resource has no evidence that there is even a project resource to query.

Ok, that makes sense, and it's certainly something that JSON API (the specification) supports. You'd just respond with:

// example of a group resource object
{
  "type": "groups",
  "id": "2",
  "relationships": {
    "projects": {
      "related": "/projects?filter[simple][group]=2"
    }
  },
  "attributes": { /* ... */ }
}

By using the related link, the client knows that there's a relationship and how to fetch the linked resources. The key is that "data" isn't included, so the server doesn't need to know the linked ids upfront.

Now, the question is how to generate the above output with this library. Part of the answer is to add a relationship in the beforeRender function that's part of the group's resource description, like so:

beforeRender: function(resource) {
  resource.relationships.projects = new RelationshipOject(undefined, /* url template for related projects here */);
  return resource;
}

where RelationshipObject is an instance of this type. That way, you can have the relationship returned in the representation without actually having it on your model.

Then, the question is just whether the library is able to serialize that relationship without the data key (the inclusion of which with null or an []would mislead clients into thinking the relationship was empty). I think the answer is currently "no" (having data is always assumed), but that should be easy to fix.

I'm off to bed now, but I'll look at this (and the Koa strategy comments) again when I wake up. I'd happily accept a PR for this issue though. All it would have to do, I think, is tell the Documentation serializer not to output data when the Relationship's linkage is undefined.

ethanresnick commented 9 years ago

This should be fixed now, such that adding the relationship in beforeRender (with undefined linkage and an appropriate template) is all you should need to do. (Note though that I've renamed RelationshipObject to simply Relationship.)

Let me know if this works for you!

shannon commented 9 years ago

Yea this works great now. Thanks!