Start a new topic

Kinvey.DataStore.update

The REST API notes: "Currently there is no way to pass only the attributes you want changed. The entire JSON body is stored as passed in the request body."



The Kinvey.DataStore.update HTML5 function and associated REST request should support only updating included attributes or "dirty" fields so that you don't need to worry about squashing data you didn't intend to update.



It would also be beneficial if you could include support for increment/decrement numeric fields and adding/removing objects to array fields.

1 person has this question

Hey Ryan, Thanks for the suggestion!
Thanks for the comment Caroline, but are there any intentions of including this as a feature? Having two users overwrite each other's changes in the same record when they hit "save" is kind of A Bad Thing.
Hey Ryan,

I know that this is definitely an issue we are addressing in new versions of the libraries, but as to timelines I'm uncertain. There are workarounds that others have found to implement through BL. Maybe I can provide examples for help?
Sure, that would be greatly appreciated!
Since saves write a whole object to the server, that means any time you update your schema, any old applications that don't have the update will continue to operate as if no changes were made. This is mostly due to not having a way to signify if a field was deleted or just never present.



One way to workaround this is to use a onPreSave business logic hook. You can diff the existing object with the incoming one and if there are fields present on the server, update the incoming object before continuing the save with those fields. This means though that you won't be able to delete fields, only reset them to a default value.



For example, if your business logic has the flow:



1. incoming save of object "1" with fields set: "a","b","c".

2. Check 1' on server - see it has fields a',b',c' and "d'".

3. Update incoming 1 so now it has fields a,b,c, and d'.

4. Continue with save 1.



In this scenario, you would never be able to delete "d" because it would keep coming back from the server version. The way around that is that you don't assign semantic meaning to "does not exist" and instead rely on the value being an existing but-empty: "", [], 0, {}, etc (or another default if that makes sense) as being the default or starting case. That way if you save the object and zero-out field "d", it will overwrite the server version, but not delete it.
Thanks Caroline - A little cumbersome but that approach should work.



Looking forward to this being addressed in new versions of the libraries!
Hey Ryan,



One other BL-centric approach that may be helpful is using mongo's $set operator (http://docs.mongodb.org/v2.6/reference/operator/update/set/#op._S_set) in combination with the update command. With a small amount of code, the operator should allow you to change only the value of the properties that changed without needing to fetch and diff against the existing object.



There are also update operators that will allow you to increase/decrease numerical fields, add to arrays, etc -- you can read all about them here http://docs.mongodb.org/v2.6/reference/operator/update/



When used within custom BL endpoints, you could essentially create tailor-made API routes that update entities the way you want them updated.
Hey Gal,



Thanks, that's a great idea! Perhaps I could use the $set operator within a onPreSave() collection hook that fires response.complete() - This way the database create/update is done inside the onPreSave().



My only thoughts are how do you verify if a user has write access to a collection and a specific document from within BL? Do you have any examples of this?
Hi Ryan, the ACL evaluation logic is a bit complicated, and unfortunately we do not currently offer a simple way to evaluate it though BL. If your use case requires taking ACLs into consideration (some use cases don't), then you might consider implementing your logic in a post-save hook, so that all ACL evaluation happens before your code is executed.
Hey Gal -- I guess I could use modules.utils.tempObjectStore to store the document on the onPreSave hook and then merge the document with the fields submitted by the update request and then update the document again on the onPostSave hook? Bit of a nasty hack :(
Hi Ryan, it's hard to give specific help without knowing more about what you're trying to do... Could you go into more detail about your use case, as it pertains to this BL?
Hi Gal, sure:



**PROBLEM:**



Step 1: Client A - Kinvey.DataStore.get to fetch a document from a collection at 8:00AM;

Step 2: Client B - Kinvey.DataStore.get to fetch the same document at 8:10AM;

Step 3: Client B - Kinvey.DataStore.update at 8:15AM updating "Name";

Step 4: Client A - Kinvey.DataStore.update at 8:20AM updating "Country".



Client A's update in Step 4 will overwrite Client B's update in Step 3, effectively overwriting the "Name" attribute back to the original value retrieved in Step 1.



**FEATURE REQUEST:**



Kinvey.DataStore.update API calls and PUT /appdata/:appKey/:collectionName/:id REST request should provide the capability to only update "dirty" or "modified" attributes, whereby only the attributes in the request are updated within the document - achieved via mongo's $set operator. This way the update in the Step 4 would not overwrite the update from Step 3. This would also decrease the "chattiness" between the client and server as full documents would not have be sent to the server when updating a small number of attributes.



**WORKAROUNDS:**



Kinvey have acknowledged that this issue may be resolved in future versions of the libraries. In the mean time, they have suggested updating the document using Business Logic hooks via direct access to the mongo $set operator. The problem with Business Logic is that there is no way to verify ACL of the incoming request if the hook is triggered by a custom endpoint - This would mean anyone could fire off a custom endpoint request and therefore overwrite any document.



You could try to use the onPreSave() and onPostSave() collection hooks on every save to the collection, whereby ACL evaluation would happen before the collection hook is executed. The problem here is that the document would be updated between the onPreSave() and onPostSave() hooks.



**IDEAS:**



1. Use modules.utils.tempObjectStore in the onPreSave() hook to cache a copy of the document before it is updated by the save - Then merge the cached document with the incoming updated attributes during the onPostSave() and update the document to this value. This would overwrite the database save called before onPostSave() with the correctly updated values (Not ideal)

2. Provide a false parameter to the response.continue utility function within the onPreSave() that then skips the actual database change and jumps directly to the onPostSave() hook. This would mean you can just make the database changes yourself while honouring ACL.



I hope this helps!



Ryan
Idea #2 above worked by using request.complete() if the onPreSave is an update and request.continue() if the onPreSave is a create, as below:



// updates document with only modified attributes from the body of a PUT request



function onPreSave(request, response, modules){

var collectionAccess = modules.collectionAccess,

collection = collectionAccess.collection(request.collectionName),

entityId = collectionAccess.objectID(request.entityId),

body = request.body,

logger = modules.logger.error;



if (request.method == 'PUT') {

collection.update({

_id: entityId

}, {

$set: body

}, function(err, count) {

if (!err) {

response.headers['X-Kinvey-Modified-Count'] = count;

response.complete();

} else {

response.body.error = 'DatabaseError';

repsonse.body.description = err.message;

response.error(400);

}

});

} else {

response.continue();

}

}



The ACL also works, by returning a 400 if the user does not have required access to the document before executing the onPreSave:



HTTP/1.1 401

Content-Type: application/json

X-Kinvey-API-Version: 3

X-Kinvey-Request-Id: 28dcdceb579e491ca0de42a13dcecb2f

X-Powered-By: Express



{

"error": "InsufficientCredentials",

"description": "The credentials used to authenticate this request are not authorized to run this operation. Please retry your request with appropriate credentials",

"debug": ""

}



It would be better if the API was able to do this without the need for custom Collection Hook code on every collection.



Ryan
Login or Signup to post a comment