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.
Preparation
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:
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 = {
"address.country": 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 => {
refreshTable(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:
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"];
updateAggregateText(count,avgPrice,resultsLimit);
})
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:
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.