bleupen / halacious

a better HAL processor for Hapi
MIT License
108 stars 22 forks source link

Embedding one entity nested in another doesn't work correctly when using toHal() #80

Open sazzer opened 8 years ago

sazzer commented 8 years ago

Embedding an entity that defines a toHal method on it inside another method with a toHal method on it works perfectly well, iff the embedding is done by use of configuration. If it is done using code in the toHal method of the outer entity then it does not work as expected.

The following works as expected:

function InnerModel(id, name, owner) {
    this.id = id;
    this.name = name;
    this.owner = owner;
}

InnerModel.prototype.toHal = function(rep, next) {
    rep.link('owner', '/users/' + this.owner.id);
    next();
}

function PageModel(items, totalCount) {
    this.items = items;
    this.totalCount = totalCount;
}

PageModel.prototype.toHal = function(rep, next) {
    rep.link('first', '?index=0'); // To prove that the toHal method runs
    next();
}

server.route({
    method: 'GET',
    path: '/items',
    config: {
        handler: (req, reply) => {
            reply(new PageModel([new InnerModel(1, 'Name', {id: 1001, name: 'Graham'})], 10));
        },
        tags: ['api'],
        plugins: {
            hal: {
                embedded: {
                    item: {
                        path: 'items',
                        href: './{item.id}'
                    }
                }
            }
        }
    }
});

And this produces the following output:

{
  "_links": {
    "self": {
      "href": "/items"
    },
    "first": {
      "href": "?index=0"
    }
  },
  "totalCount": 10,
  "_embedded": {
    "item": [
      {
        "_links": {
          "self": {
            "href": "/items/1"
          },
          "owner": {
            "href": "/users/1001"
          }
        },
        "id": 1,
        "name": "Name",
        "owner": {
          "id": 1001,
          "name": "Graham"
        }
      }
    ]
  }
}

However, if I try and do this with code instead, as follows:

function InnerModel(id, name, owner) {
    this.id = id;
    this.name = name;
    this.owner = owner;
}

InnerModel.prototype.toHal = function(rep, next) {
    rep.link('owner', '/users/' + this.owner.id);
    next();
}

function PageModel(items, totalCount) {
    this.items = items;
    this.totalCount = totalCount;
}

PageModel.prototype.toHal = function(rep, next) {
    this.items.forEach(function(i) {
        rep.embed('items', './' + i.id, i);
    });
    rep.ignore('items');
    rep.link('first', '?index=0'); // To prove that the toHal method runs
    next();
}

server.route({
    method: 'GET',
    path: '/items',
    config: {
        handler: (req, reply) => {
            reply(new PageModel([new InnerModel(1, 'Name', {id: 1001, name: 'Graham'})], 10));
        },
        tags: ['api'],
        plugins: {
            hal: {
            }
        }
    }
});

And this produces the following output:

{
  "_links": {
    "self": {
      "href": "/items"
    },
    "first": {
      "href": "?index=0"
    }
  },
  "totalCount": 10,
  "_embedded": {
    "item": [
      {
        "_links": {
          "self": {
            "href": "/items/1"
          }
        },
        "id": 1,
        "name": "Name",
        "owner": {
          "id": 1001,
          "name": "Graham"
        }
      }
    ]
  }
}

Note in this case the inner Item does not have a link of "owner" even though the actual InnerModel class is exactly the same in both cases. The only difference is the one embeds the items using the "embedded" configuration and the other does it using "rep.embed" in the toHal method.

Note that I wanted to do it this way so that I can have a single PageModel class that is used everywhere instead of repeating the configuration everywhere that I want to do this.

alexb-uk commented 6 years ago

Unless I have missed something this is still an issue in ^5.0.1.

Does anyone know of a workaround?

Thanks

bleupen commented 6 years ago

Hey there,

I need to go through and clean up these old tickets. The best way to implement toHal() now is to make use of the entity's configure() method. its simpler and it works just like configuring the route:

        PageModel.prototype.toHal = function (rep, next) {
            rep.configure({
                embedded: {
                    item: {
                        path: 'items',
                        href: './{item.id}'
                    }
                }
            }, next);
        };
alexb-uk commented 6 years ago

Hi @bleupen,

Thanks a lot for the heads-up, that's really helped simplify a lot of my models toHal() methods and HAPI plugin config.

Cheers

Alex