Geospatial SPA using JavaScript only - part 2

This post is 4 years old. (Or older!) Code samples may not work, screenshots may be missing and links could be broken. Although some of the content may be relevant please take it with a pinch of salt.

In the first part of this article series I have discussed the database backend and the JSON document structure. In this part we'll dwelve into how the node.js/Express setup looks like.

As a reminder let's go through what kind of documents we actually store in the database. We have JSON documents that contain information such as the filename of the image, the extracted geospatial datapoint and some other pieces of information. These JSON documents are all stored in a collection called image. We are also storing binary documents (JPEG images) in the database and they are part of a collection called 'binary'.

Querying these documents from node.js is possible via our node-client-api. (For a detailed explanation please read this article)

Let's take a moment to think about what we're trying to achieve first. We'll need to create endpoints that later on can be consumed from a frontend service (spoiler alert: we'll be using Angular).

  • There should be an endpoint that lists all the data from the two collections
  • There should be another endpoint that displays information about one single image

We have to bear in mind that we are not only building an api but we are also going to serve our HTML pages from Express therefore we have to create endpoints for them as well. In light of this it would make sense to separate out the routes into 'app' routes and 'api' routes.

Note that the approach that I am following here is one out of many possible. If you don't like how I setup my routes feel free to use your own approach.

Based on the points above I have created a routes.js file which exports an app and an api object, which is than accessed from the main app.js:

var routes = require('./routes').app;
var apiroutes = require('./routes').api;

And here is the actual route configuration using Express4:

router.route('/').get(routes.index);
router.route('/api/').get(apiroutes.index);
router.route('/api/:id').get(apiroutes.image);

If you want, you can use a more purposeful library to create the API such as hapi

Route definitions inside Express accepts a callback function that has to have a request and a response parameter, therefore the callbacks that you see inside the .get() methods are function expressions.

The most simple one of course is routes.index as it simply renders the index.jade file which is the 'homepage' of the application:

var appindex = function (req, res) {
res.render('index');
};

Note that both res.render('index.jade') and res.render('index') yield the same results

Let's have a look at apiroutes.index which is a function responsible for displaying all the documents from the database - but I like to logically separate the functionality therefore the route defintion will call another function that does the heavy-lifting:

var apiindex = function (req, res) {
selectAll()
.then(function (documents) {
res.json(documents);
})
.catch(function (error) {
console.log('Error: ', error);
});
};

From the code snippet above you can probably already guess that selectAll() returns a promise as I'm calling .then() on it.

var selectAll = function selectAll() {
return db.documents
.query(
qb
.where(qb.collection('image'))
.orderBy(qb.sort('filename'))
.slice(0, 300) //return 300 documents "per page" (pagination)
)
.result();
};

selectAll() simply queries for all the documents that belong to a collection called 'image' and sorts them by an index, which also means that there must be an index setup on the 'filename' key. In MarkLogic this is an Element Range Index. The Element Range Index with a String datatype helps to create unique values inside the index and also allows us to sort the documents inside the database alphabetically. This is required as later on we are going to list the files using ng-repeat in AngularJS. If we wouldn't have the alphabetical order the documents would always come back in a random order and we wouldn't be able to make use of sorting inside the ng-repeat loop.

As mentioned before the above function return a promise, so let's have a look at the function that calles selectAll():

var apiindex = function (req, res) {
selectAll()
.then(function (documents) {
res.json(documents);
})
.catch(function (error) {
console.log('Error: ', error);
});
};

If you have used Express before you'll already know that what you see here is the callback function that will be passed as a parameter to a routing function. Each of these routing functions must have a req(uest) and a res(ponse) parameter.

What you see above there is an anyonymus function expression which calls the selectAll() function and handles the two promises states - resolve and reject by calling the .then() and .catch() methods. If the promise is fullfilled then we simply return all the documents as a JSON response.

Here's a screenshot from Postman (my preferred REST testing tool) to see the result of this function:

We now have all the JSON documents returned to us - great success. Let's have a look at a function now that would return the information of one single JSON document.

First we of course need to specify the appropriate endpoint:

router.route('/api/:id').get(apiroutes.image);

Let's take a look at how this function is implemented in the route:

var selectOne = function selectOne(uri) {
return db.documents.read('/image/' + uri + '.json').result();
};

Notice that now instead of doing a query against the documents in the database we are using the .read() method to simply read the content of one single document.

var apiimage = function (req, res) {
var id = req.params.id;
selectOne(id)
.then(function (document) {
if (document.length !== 0) {
res.json(document);
} else {
// this 404 is captured via an AngularJS HTTP Interceptor
res.status(404).end();
}
})
.catch(function (error) {
console.log('Error: ', error);
});
};

In Express we can access the parameters that are sent with our requests by accessing the req.params object with the property that we have specified in the routing configuration. If we find a document we send back the document's content in a JSON format otherwise we set the HTTP status to be 404. This scenario will later be handled by AngularJS via an HTTP Interceptor.

There is one final piece missing of course - if you have noticed we are only retrieving JSON documents but not the actual images. Let's create a dedicated endpoint that will do this for us:

router.route('/api/imagedata/:id').get(apiroutes.imagedata);

The helper function looks similar to the one we saw before:

var selectImageData = function selectImageData(uri, callback) {
return db.documents.read('/binary/' + uri).result();
};

However when it gets called we need to modify the data that is being returned by the MarkLogic server:

var apiimagedata = function (req, res) {
var id = req.params.id;
selectImageData(id)
.then(function (binaryData) {
res.json(new Buffer(binaryData[0].content, 'binary').toString('base64'));
})
.catch(function (error) {
console.log('Error: ', error);
});
};

We are returning a base64 encoded binary value - we are going to use this to rebuild the image in the frontend.

In the next article we'll have a look at how to save data to the database from our API and also have a look at how relevancy ranked search is implemented.