Icenium Everlive – Telerik's backend-as-a-service platform – makes storing data in the cloud very simple. I recently wrote a post about uploading files to Everlive - and we did so by using Everlive's HTTP API. Here's a snippet from the sample app created for that post, showing the behavior executed when an image viewmodel (which includes a title and file, etc.) is saved to Everlive:
var addImageViewModel = { // other viewmodel members here…. saveItem: function () { var that = this; $newPicture = $newPicture || $("#newPicture"); AppHelper.getImageFileObject( $newPicture[0].files[0], function (err, fileObj) { if (err) { navigator.notification.alert(err); return; } $.ajax({ type: "POST", url: 'https://api.everlive.com/v1/wEx9wdnIcxxehNty/Files', contentType: "application/json", data: JSON.stringify(fileObj), error: function (error) { navigator.notification.alert(JSON.stringify(error)); } }).done(function (data) { var item = imagesViewModel.images.add(); item.Title = that.get('picTitle'); item.Picture = data.Result.Id; imagesViewModel.images.one('sync', function () { mobileApp.navigate('#:back'); }); imagesViewModel.images.sync(); // reset the form that.set("picSelected", false); $newPicture.replaceWith($newPicture = $newPicture.clone(true)); }); } ); } };
In the saveItem
method above, you can see that we're calling AppHelper.getImageFileObject
. That method ultimately takes the image, reads it, base64 encodes it, and creates an object to be used as the data of a POST back to Everlive. That object is passed into the callback argument (second arg) - from there we make an AJAX call to upload the file, and we chain a done
callback to be executed when the upload completes successfully. It's inside that done
callback argument that we finally save the original "record" that the image is associated with. The flow is straightforward:
For just one upload at a time, this isn't horrible. However, if we're writing an application that could allow the user to upload several photos at a time, this not-too-awful-but-could-improve code will exponentially increase, becoming Dreaded Boilerplate™.
I hate boilerplate - so let's look at one way to reduce it. While I plan to investigate other options in the future - batching file uploads or using Everlive "cloud code" hooks to process all the items from one request (record + images) – for this post we're going to focus on something simpler: iterating over any "file" input elements, uploading the chosen files, and compiling an object containing the IDs Everlive generates for each saved file. I've created a proof-of-concept utility lib to help manage this: everloader.
Let's look at how it might help out. The snippet below is our new saveItem
method (you can see the sample project here):
var addImageViewModel = { // other viewmodel members here…. saveItem: function () { var that = this; everloader .upload() .then(function (data) { var item = imagesViewModel.images.add(); item.Title = that.get('picTitle'); item.Picture = Object.keys(data.newPicture)[0]; imagesViewModel.images.one('sync', function () { mobileApp.navigate('#:back'); }); imagesViewModel.images.sync(); // reset the form that.set("picSelected", false); $newPicture.replaceWith($newPicture = $newPicture.clone(true)); }, function (data) { var msg = JSON.stringify(data.errors, null, 2); alert("There was a problem:\n" + msg); }); } }
We managed to shave off some code bloat! Notice that instead of the call to AppHelper.getImageFileObject
and the subsequent $.ajax
request to upload the file, we just have everloader.upload()
. The data
argument that gets passed into our then
callback will contain the IDs for any images uploaded to Everlive as part of the save operation. Notice the line:
item.Picture = Object.keys(data.newPicture)[0];
You can infer from that line that the data
argument will have a property for every file input element id, and the value will be an object whose members correspond to file IDs returned from Everlive (and the values contain additional file metadata). In the example above, we just uploaded one file. Uploading multiple files would only require us to use the additional IDs that uploading multiple files would return from the server.
So - what have we gained?
The nice thing is, we've not only moved the original file upload logic behind everloader.upload()
, but we've done it in such a way to allow any file input to be processed. (You can configure which input elements are processed, but the default is any with type="file"
.) The upload()
method then returns a promise, allowing us to continue saving our viewmodel. You can customize everloader along these lines:
getFileInputs
method to determine which input elements are used for uploading files (as mentioned above)mimeTypes
array (leaving this array empty means any file type can be uploaded)While reducing the amount of code is a nice win, we've gained something more in taking this kind of approach. We've abstracted infrastructure code away from app logic, and made our app logic the focus in the example above. We just want our addImageViewModel
to do it's thing: save the record we created. The 'narrative' of our code shouldn't have to be crowded with the how of uploading files. By moving the how into an "infrastructure" layer, we're able to make the saveItem
method's intent clearer, and we've gained the use of specialized behavior we might want to share between apps.
The everloder lib is only one example of the kinds of utilities enabled by great backend services like Icenium Everlive. I'd love your feedback, but I'd also love to hear about the kinds of utilities you envision being useful working with cloud-based backends.
Jim Cowart is an architect, developer, open source author, and overall web/hybrid mobile development geek. He is an active speaker and writer, with a passion for elevating developer knowledge of patterns and helpful frameworks.