Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support bidirectional links #81

Open
kristofsajdak opened this issue May 18, 2015 · 1 comment
Open

support bidirectional links #81

kristofsajdak opened this issue May 18, 2015 · 1 comment

Comments

@kristofsajdak
Copy link

Managing a bidirectional relationship on both ends could leave data in an inconsistent state

Mongodb doesn't offer any transactional semantics, the update might succeed on the first document and fail on the second one

Here is an example, let's say you want to change an article's 'author' relationship (targets people resource)

In our case this is bidirectional and the people resource also has an 'articles' relationship, so you also need to reflect that the current author no longer has a relation to the article in question

you could start out by updating the articles 'author' rel, but run into a failure while updating the people 'articles' rel, conversively you could start by updating the people 'articles' rel, but run into a failure while updating the articles 'author' rel

Harvester eliminates the risk by exposing an 'inverse' link attribute, this attribute informs Harvester that the relationship is actually being managed on the other side of the relationship

The link can now be updated with 1 atomic operation to reflect the change on both ends

 var articles = harvester
        .resource('articles', {
            title: Joi.string().required(),
            links: {
                author: 'people'
                comments: [{type: 'comments', inverse: 'article'}]
            }
        });

 var comments = harvester
        .resource('comments', {
            body: Joi.string().required(),
            links: {
                article: 'articles'
            }
        });

 var people = harvester
        .resource('people', {
            'first-name': Joi.string().required(),
            'last-name': Joi.string().required(),
            twitter: Joi.string().required(),
            links: {
                articles: [{type: 'articles', inverse: 'author'}]
            }
        });

The managed side of the relationship outputs linkage objects with the ids, as well as 'related' and 'self' links, the non-managed side of the relationship outputs 'related' and 'self' links only

Pro tip: Model the resource so that ids are managed on the one-side, not on the many-side, this is more efficient as less data has to be transferred when retrieving / updating a resource

{
  "data": [{
    "type": "articles",
    "id": "1",
    "attributes": {
      "title": "JSON API paints my bikeshed!"
    },
    "links": {
      "self": "http://example.com/articles/1",
      "author": {
        "self": "http://example.com/articles/1/links/author",
        "related": "http://example.com/articles/1/author",
        "linkage": { "type": "people", "id": "9" }
      },
      "comments": {
        "self": "http://example.com/articles/1/links/comments",
        "related": "http://example.com/articles/1/comments"
        ]
      }
    }
  }]
}   

Important : The inclusion mechanism materialises the link so that a full data graph can be resolved as part of a compound document

Given the following resource url : /articles?include=comments
the comments link object is transformed with the addition of linkage objects, the object ids can subsequently be used to match the included comments resource data

{
  "data": [{
    "type": "articles",
    "id": "1",
    "attributes": {
      "title": "JSON API paints my bikeshed!"
    },
    "links": {
      "self": "http://example.com/articles/1",
      "author": {
        "self": "http://example.com/articles/1/links/author",
        "related": "http://example.com/articles/1/author",
        "linkage": { "type": "people", "id": "9" }
      },
      "comments": {
        "self": "http://example.com/articles/1/links/comments",
        "related": "http://example.com/articles/1/comments",
        "linkage": [
          { "type": "comments", "id": "5" },
          { "type": "comments", "id": "12" },
          { "type": "comments", "id": "13" },
          { "type": "comments", "id": "14" },
          { "type": "comments", "id": "15" },
          { "type": "comments", "id": "16" },
          { "type": "comments", "id": "17" },
          { "type": "comments", "id": "18" },
          { "type": "comments", "id": "19" },
          { "type": "comments", "id": "20" },
          { "type": "comments", "id": "21" },
          { "type": "comments", "id": "22" },
          { "type": "comments", "id": "23" },
          { "type": "comments", "id": "24" },
          { "type": "comments", "id": "25" },
          { "type": "comments", "id": "26" },
          { "type": "comments", "id": "27" },
          { "type": "comments", "id": "28" },
          { "type": "comments", "id": "29" },
          { "type": "comments", "id": "30" },
          { "type": "comments", "id": "31" },
          { "type": "comments", "id": "32" },
          { "type": "comments", "id": "33" },
          { "type": "comments", "id": "34" },
          { "type": "comments", "id": "35" },
          { "type": "comments", "id": "36" },
          { "type": "comments", "id": "37" },
          { "type": "comments", "id": "38" },
          { "type": "comments", "id": "39" },
          { "type": "comments", "id": "40" },
          { "type": "comments", "id": "41" },
          { "type": "comments", "id": "42" },
          { "type": "comments", "id": "43" },
          { "type": "comments", "id": "44" },
          { "type": "comments", "id": "45" },
          { "type": "comments", "id": "46" },
          { "type": "comments", "id": "47" },
          { "type": "comments", "id": "48" },
          { "type": "comments", "id": "49" },
          { "type": "comments", "id": "50" },
          { "type": "comments", "id": "51" },
          { "type": "comments", "id": "52" },
          { "type": "comments", "id": "53" },
          { "type": "comments", "id": "54" },
          { "type": "comments", "id": "55" },
          { "type": "comments", "id": "56" },
          { "type": "comments", "id": "57" },
          { "type": "comments", "id": "58" },
          { "type": "comments", "id": "59" },
          { "type": "comments", "id": "60" },
          { "type": "comments", "id": "61" },
          { "type": "comments", "id": "62" },
          { "type": "comments", "id": "63" },
          { "type": "comments", "id": "64" },
          { "type": "comments", "id": "65" },
          { "type": "comments", "id": "66" }
          ... bla bla bla 
      }
    }
  }],
  "included": [{
    "type": "people",
    "id": "9",
    "attributes": {
      "first-name": "Dan",
      "last-name": "Gebhardt",
      "twitter": "dgeb"
    },
    "links": {
      "self": "http://example.com/people/9"
    }
  }, {
    "type": "comments",
    "id": "5",
    "attributes": {
      "body": "First!"
    },
    "links": {
      "self": "http://example.com/comments/5"
    }
  }, {
    "type": "comments",
    "id": "12",
    "attributes": {
      "body": "I like XML better"
    },
    "links": {
      "self": "http://example.com/comments/12"
    }
  }, {
    "type": "comments",
    "id": "12",
    "attributes": {
      "body": "I like XML better"
    },
    "links": {
      "self": "http://example.com/comments/12"
    }
  }, {
    "type": "comments",
    "id": "13",
    "attributes": {
      "body": "I like Nutella better"
    },
    "links": {
      "self": "http://example.com/comments/13"
    }
  }, {
    "type": "comments",
    "id": "14",
    "attributes": {
      "body": "I like Peanutbutter better"
    },
    "links": {
      "self": "http://example.com/comments/14"
    }
  }, {
    "type": "comments",
    "id": "15",
    "attributes": {
      "body": "I like Choco Pops better"
    },
    "links": {
      "self": "http://example.com/comments/15"
    }
  }
  ... more comments 
  ]
}
@kristofsajdak
Copy link
Author

@ssebro Here is an example on how we could define a many-to-many relationship

 var articles = harvester
        .resource('articles', {
            title: Joi.string().required(),
            links: {
                author: 'people',
                contributors: ['people'] 
                comments: [{type: 'comments', inverse: 'article'}]
            }
        });

 var people = harvester
        .resource('people', {
            'first-name': Joi.string().required(),
            'last-name': Joi.string().required(),
            twitter: Joi.string().required(),
            links: {
                articlesAuthored: [{type: 'articles', inverse: 'author'}]
                articlesContributed: [{type: 'articles', inverse: 'contributors'}]
            }
        });
{
  "data": [{
    "type": "articles",
    "id": "1",
    "attributes": {
      "title": "JSON API paints my bikeshed!"
    },
    "links": {
      "self": "http://example.com/articles/1",
      "author": {
        "self": "http://example.com/articles/1/links/author",
        "related": "http://example.com/articles/1/author",
        "linkage": { "type": "people", "id": "9" }
      },
      "contributors": {
        "self": "http://example.com/articles/1/links/contributors",
        "related": "http://example.com/articles/1/contributors",
        "linkage": [
               { "type": "people", "id": "10" },
               { "type": "people", "id": "11" }
        ]
      },
      "comments": {
        "self": "http://example.com/articles/1/links/comments",
        "related": "http://example.com/articles/1/comments"
        ]
      }
    }
  }]
}   

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant