Solving the JAM Session Challenge

Dj Walker-Morgan

#MongoDB World 2019

This challenge was the penultimate week of Eliot's Weekly MongoDB World Challenge - week 5's JAM Session Challenge. We set you the task of creating the missing database code in a web application with Stitch's QueryAnywhere. Once you'd done that all you needed to do was to get that application online with Stitch Hosting. You can read the full details of the week 5 challenge to refresh yourself before we head into the solution.


When creating the rule for the database access, remember to start with the "No Template" template. Then add the listed fields, only checking the read access check boxes for those fields. It should look like this when done:

Setting the read-only access

Creating The Code

The instructions in the challenge direct you to first put your stitch app id into the code, then head down to around line 50.

That part of the code will look like this:

// Function to query MongoDB for relevant data before populating UI
function searchAndBuild() {
  //Get the current selections from the dropdowns
  let countryChoice = lists.namedItem("countryList").value;
  let bathroomsChoice = parseInt(lists.namedItem("bathroomsList").value);
  let bedroomsChoice = parseInt(lists.namedItem("bedroomsList").value);
  let priceChoice = parseInt(lists.namedItem("priceList").value);
  let typeChoice = lists.namedItem("typeList").value;
  let resultsLimit = parseInt(lists.namedItem("resultsList").value);

  // Construct a query to get the matching properties (limited to the number of results)
  // and then call the following function:
  // refreshTable(matchingData)

  // Construct an aggregation pipeline that returns the averagePrice and
  // count of ALL matching properties and then call the following function:
  // updateAggregateText(count, avgPrice, resultsLimit);

Finding the matches

The first thing to build is our query. This will take nearly all the values from the selections and assemble them into a MongoDB query object:

const query = {
    "": countryChoice,
    "bathrooms": { "$gte": bathroomsChoice },
    "bedrooms": { "$gte": bedroomsChoice },
    "price": { "$lte": priceChoice },
    "property_type": typeChoice

Now this is a typical MongoDB query with the various criteria implicitly anded together. There is one selection unused though and that's the results limit. That is handled through options, so let us create that:

  const options = {
    "limit": resultsLimit

Now you may have added a projection to remove the _id field:

 const options = {
    "limit": resultsLimit,
    "projection": { "_id": 0 }

Or even specifically set particular fields to appear:

 const options = {
    "limit": resultsLimit,
    "projection": { "_id": 0, "address": 1, "bathrooms": 1, "bedrooms": 1, "name": 1, "price": 1, "property_type": 1}

Both of which are perfectly legitimate things to do but remember our rules set what fields would be returned so listing them again is unnecessary duplication in this challenge. There's not much value in just removing the _id either. If you look down to the buildRow function, you'll find the _id and address are stripped out of displayed rows anyway. So we only need to set the limit.

We are now ready to send our query to the database:

 coll.find(query,options).asArray().then(tableData => {

We get all the results as an array using the asArray() function which returns a promise which we take up with the then(), refreshing the table.

Save that at this point and load up index.html into a browser and your should see something like this:

A Query but no pipeline

The "Loading…" message doesn't go away because that's the next part of the challenge.

Creating the pipeline

Now we need to use the query to perform an aggregation. To do that we create an aggregation pipeline to run:

  const pipeline = [
    { "$match": query },
      "$group": {
        "_id": null,
        "averagePrice": { "$avg": "$price" },
        "count": { "$sum": 1 }

This pipeline's first stage is simply matching using $match. It will pass on all the documents which match our query to the next stage. That stage is a $group stage. With an _id of null, it will process all the incoming documents according to the following fields and aggregation operators. The first field is averagePrice and that's created by calculating the average ($avg) of the price field. The second field is count and that simply adds ($sum) a constant one to the running count.

Now we can run the aggregation:

coll.aggregate(pipeline).toArray().then( aggregateData => {
    let avgPrice=aggregateData[0]["averagePrice"];
    let count=aggregateData[0]["count"];

We extract the aggregated data from the returned array and send it on to the updateAggregateText function which will round the average price down to an integer and replace the "Loading" message with our new information. It should look like this:

The query with the pipeline

And we're done. All that is needed now it to create some Stitch static hosting, copy your files up to the new host, flush the CDN cache and connect to that host to make sure it all works.