In my last few posts, I’ve been working on an Azure Mobile Apps replacement service. It will run in Azure Functions and use DocumentDb as a backing store. Neither of these requirements are possible in the Azure Mobile Apps server SDK today. Thus far, I’ve created a CRUD HTTP API , initialized the DocumentDb store andhandled inserts. Today is all about fetching, but more importantly it is about replacing documents and handling conflict resolution.
The DocumentDb DriverBefore I get started with the code for the endpoint, I need to add some more functionality to my DocumentDb promisified driver. In the document.js file, I’ve added the following:
module.exports = {createDocument: function (client, collectionRef, docObject, callback) {
client.createDocument(collectionRef._self, docObject, callback);
},
fetchDocument: function (client, collectionRef, docId, callback) {
var querySpec = {query: 'SELECT * FROM root r WHERE [email protected]',parameters: [{ name: [email protected]', value: docId}]
};
client.queryDocuments(collectionRef._self, querySpec).current(callback);
},
readDocument: function (client, docLink, options, callback) {
client.readDocument(docLink, options, callback);
},
replaceDocument: function(client, docLink, docObject, callback) {
client.replaceDocument(docLink, docObject, callback);
}
};
My first attempt at reading a document used the readDocument() method. I would construct a docLink using the following:
var docLink = `${refs.table._self}${refs.table._docs}${docId}`;However, this always resulted in a 400 Bad Request response from DocumentDb. The reason is likely that the _self link uses the shorted (and obfuscated) URI, whereas the Document Id I am using is a GUID and is not obfuscated. If you take a look at the response from DocumentDb, there is an id field and a _rid field. The _rid field is used in the document links. Thus, instead of using readDocument() , I’m using a queryDocuments() call on the driver to search for the Id. I’ve also promisified these calls in the normal manner using the Bluebird library.
Fetching a RecordThe Azure Mobile Apps SDK allows me to GET /tables/todoitem/id where id is the GUID. With the driver complete, I can do the following in the Azure Function table controller:
function getOneItem(req, res, id) {driver.fetchDocument(refs.client, refs.table, id)
.then((document) => {
if (typeof document === 'undefined')res.status(404).json({ 'error': 'Not Found' });
elseres.status(200).json(convertItem(document));
})
.catch((error) => {
res.status(error.code).json(convertError(error));
});
}
When doing this, I did notice that some semantics seem to have changed in the Azure Functions SDK. I can no longer use context.bindings.id and has to switch to using req.params.id . Aside from this small change in the router code, this code is relatively straight forward. I established the convertItem() and convertError() methods in my last article.
Replacing a RecordThe more complex case is replacing a record. There is a little bit of logic around conflict resolution:
If there is an If-Match header, then ensure the version of the current record matches the If-Match header, otherwise return a 412 response. If there is no If-match header, but the new record contains a version, return a 409 response. Otherwise update the recordBecause we want the version and updatedAt fields to be controlled as well, we need to ensure the new object does not contain those values when it is submitted to DocumentDb:
function replaceItem(req, res, id) {driver.fetchDocument(refs.client, refs.table, id)
.then((document) => {
if (typeof document === 'undefined') {res.status(404).json({ 'error': 'Not Found' });return;
}
var item = req.body, version = new Buffer(document._etag).toString('base64')
if (item.id !== id) {res.status(400).json({ 'error': 'Id does not match' });return;
}
if (req.headers.hasOwnProperty('if-match') && req.header['if-match'] !== version) {res.status(412).json({ 'current': version, 'new': item.version, 'error': 'Version Mismatch' })return;
}
if (item.hasOwnProperty('version') && item.version !== version) {res.status(409).json({ 'current': version, 'new': item.version, 'error': 'Version Mismatch' });return;
}
// Delete the version and updatedAt fields from the doc before submitting
delete item.updatedAt;
delete item.version;
driver.replaceDocument(refs.client, document._self, item)
.then((updatedDocument) => {res.status(200).json(convertItem(updatedDocument));return;
});
})
.catch((error) => {
res.status(error.code).json(convertError(error));
});
}
I’m using the same Base64 encoding for the etag in the current document to ensure I can do a proper match. I could get DocumentDb to do all this work for me the options value in the driver replaceDocument() method allows me to specify an If-Match. However, to do that, I would need to still fetch the record (since I need the document link), so I may as well do the checks myself. This also keeps some load off the DocumentDb, which is helpful.