Loopback: Appending data to a model's Array type property

Created on 20 Feb 2015  路  11Comments  路  Source: strongloop/loopback

Description

I'm trying to achive appending of objects into a property (array type) of a document that already exists, using _PUT_.

Exemple:

I have the following object into my database:

{
  _id: "154",
  createdAt: "2015-02-20T11:50:00:000Z",
  events: [
    { action: "creation", from:"Koren"}
  ]
}

_events_ is described in my model with type: ["object"]

I try to obtain:

{
  _id: "154",
  createdAt: "2015-02-20T11:50:00:000Z",
  events: [
    { action: "creation", from:"Koren"},
    { action: "reply", from: "raymondfeng"}
  ]
}

By sending:

  id: "154",
  events: [
    { action: "reply", from: "raymondfeng"}
  ]

In the _PUT_ request.

What I obtained

{
  _id: "154",
  createdAt: "2015-02-20T11:50:00:000Z",
  events: [
    { action: "reply", from: "raymondfeng"}
  ]
}

This seems logical to me, as I requested an update on an existing field, it was replaced.
However, is there a way to achieve what I tried to do with loopback ?
Can we imagine more remote methods bundled with loopback to do this with array types ?

Maybe like relations, doing a POST on MyModel/{id}/MyProperty for array types

Most helpful comment

While waiting for your response, I ended up with this code.
That does the job for me (caution with this snipped you are able to append but you can't delete from an embedded array. This is also bad performance since you have to query the DB to fetch and then save data again. _Brrrrr bad bad_).

MyModel.observe('before save', function appendEvents(ctx, next) {
    if (ctx.data !== undefined && ctx.data.events !== undefined) {
      MyModel.findOne({
        where: ctx.where
      }, function(err, myModel) {
        if (err === null && typeof myModel.events === 'object') {
          for (var i = 0; i < ctx.data.events.length; i++) {
            myModel.events.push(ctx.data.events[i]);
          }
          ctx.data.events = myModel.events;
        }
        next();
      });
    } else {
      next();
    }
  });

I'm still looking for more info on how to achieve this because I'm not realy satisfied by what I've achieved

All 11 comments

If you model your 'events' array as an embedsMany relation, you can treat it just like a regular relation, and thus, append using POST /api/things/:id/events.

Can you provide me an exemple ?
I tried:

"relations": {
    "events": {
      "type": "embedsMany",
      "model": "object"
    }
}

Nothing shows off

I tried also to create an empty PersistedModel called "empty" as below:

{
  "name": "empty",
  "base": "PersistedModel",
  "properties": {},
  "validations": [],
  "relations": {},
  "acls": [],
  "methods": []
}

Set it as dataSource: "db" in the model-config because otherwise it is not exposed on API, and used for relation:

"relations": {
    "events": {
      "type": "embedsMany",
      "model": "empty"
    }
}

But it creates an "empties" property of type array inside my document (shouldn't it be "events" ?) that now looks like:

{
  id: "154",
  empties: []
}

Is it normal behaviour ?

Yes, this is normal - you're well on your way with this solution. The relationship accessor will be called events, yet the property is called empties (however, you can set the as: property: '_events' for example). Note that the actual property and relationship accessor cannot have the same name, because they would clobber.

Thanks for your answer.
Now last step, when I try to create the model, I have the following error:

{"error":
  { "name":"ValidationError",
    "status":422,
    "message": "The `MyModel` instance is not valid. Details: `events` contains invalid item at index `0`: `id` is blank (value: [ { id: undefined,\n    lo...} ])."
  }
}

So I tried with this in the model relation (as documented here at bottom: http://docs.strongloop.com/display/public/LB/Embedded+models+and+relations)

"options": {
  "validate": false,
  "autoId": true
}

But the same error is triggered.

Any idea how to bypass this?

While waiting for your response, I ended up with this code.
That does the job for me (caution with this snipped you are able to append but you can't delete from an embedded array. This is also bad performance since you have to query the DB to fetch and then save data again. _Brrrrr bad bad_).

MyModel.observe('before save', function appendEvents(ctx, next) {
    if (ctx.data !== undefined && ctx.data.events !== undefined) {
      MyModel.findOne({
        where: ctx.where
      }, function(err, myModel) {
        if (err === null && typeof myModel.events === 'object') {
          for (var i = 0; i < ctx.data.events.length; i++) {
            myModel.events.push(ctx.data.events[i]);
          }
          ctx.data.events = myModel.events;
        }
        next();
      });
    } else {
      next();
    }
  });

I'm still looking for more info on how to achieve this because I'm not realy satisfied by what I've achieved

I think you should try inheriting from a model that is tied to a Transient datasource - there are examples of this in the relation tests. Your model will need an id, in order for the relationship to be valid (so you can update or delete by id, for example).

The Transient connector will handle autoId creation: https://github.com/strongloop/loopback-datasource-juggler/blob/master/lib/connectors/transient.js

Please post questions to https://groups.google.com/forum/#!forum/loopbackjs. See https://github.com/strongloop/loopback/wiki/Reporting-issues#question for more details.

@fabien Since you are already working on this issue, I will leave it open (the new procedure is to redirect questions to the Google group).

@superkhau in that case, it's probably best to close this.

Try with this:

{
  "name": "empty",
  "base": "PersistedModel",
  "idInjection":true,
  "properties": {},
  "validations": [],
  "relations": {},
  "acls": [],
  "methods": []
}

I tried it but ID is not created, so I generated an UUID client-side before insert :/
If I do 2 concurrent POST on the relation, one of the post is not appended, so I think its not ready yet

Issue is closed since it is tagged as a 'question' but I think it is more like a bug since there is no reliable way to append data to an array property (fast queries are losing data). Since it was not the original topic of this issue, I opened another issue here: https://github.com/strongloop/loopback/issues/1178

I ended with implementing worker pools client side, and using POST on the document model relation one by one with the pool, so I'm sure data is consistent in DB

perhaps this is resolved somewhere else, however i'll leave a comment with the solution that worked for me for whomever would land here.

in the model that gets embed, i've explicitly defined an id property field and defaulted it's value to uuid

JSON :
"properties": {
... your properties
"id": {
"type": "string",
"id": true,
"defaultFn": "uuid"
}
},

it worked for me

Was this page helpful?
0 / 5 - 0 ratings