Zza! (Node/MongoDB)
Zza! is a single page application for ordering pizzas, salads and drinks.
This is the 100% JavaScript version of Zza! written for Node.js running Express with a MongoDB database.
It's built on the BMEAN stack:
Breeze | client-side data modeling and data access |
Mongo | server-side document-oriented database |
Express | server-side web application framework on node |
Angular | client-side presentation framework |
Node | server-side platform for web applications |
Breeze works well with Microsoft technologies as we've demonstrated many times. It also works well outside the Microsoft ecosystem. To drive that point home, this sample has ...
no ASP.NET
no Web API
no IIS
no Entity Framework
no SQL Server
Download
Download the sample and unzip it..
Within the unzipped directory you will find:
- a readme.md explaining how to install and run the app
- the Zza! Mongo database
- a folder full of HTML, CSS, and JavaScript
You can view, edit, and run the code using the tools of your choice.
Run it!
The readme.md has the details. Just remember the three basic steps:
- Start your MongoDB server:
mongod
- Start the Node/Express server:
node server
- Launch a browser and navigate to
localhost:3000
The app should load on the home screen with a big picture of "Zza" himself. Click the "Order" link in the menu bar and you'll see what's on the menu beginning with pizzas.
The "Salad" and "Drinks" links switch the view to similar views of those products.
You want the "Holy Smokes" pizza so you click on it. You're taken to a screen where you can pick the size and add toppings. Go for the "Large", pick the "Wheat Crust", and pile on some "BBQ Chicken" from the "Meat" toppings. You're hungry so why not order two of these babies?
The dashboard on the left displays that we're considering a "Holy Smokes" pizza but haven't put it in our cart yet. Click the "Add to Cart" button and it moves up the dashboard into the "Order Summary" panel.
Click the "View Cart" link (or the "Cart" button on the upper-right) and we'll see our order so far:
Go back to "Order" and load up on Salads and Drinks.
Inside the app
A single web page, index.html, frames the story. There's only a tiny bit of layout:
<body class="ng-cloak" data-ng-app="app" data-ng-controller="appController"> <div data-ng-include="'app/views/shellHeader.html'"></div> <div data-ng-view style="margin-top: 50px"></div> <div data-ng-include="'app/views/shellFooter.html'"></div> <!-- 3rd party javascript libraries --> <!-- App javascript libraries --> </body>
Angular.js
Angular.js is running the show. It asynchronously loads header and footer HTML.
In the middle, inside the <div> with the data-ng-view
attribute, it dynamically loads a view based on a "route". The view could be the "Home" page with the picture of "Zza"; it could be the "About" page; it could be a product menu page, an order page, or the cart page. It all depends upon the route ... which you see in the address bar:
In this example, the route identifies the pizza product whose id is "3". Angular uses route information (in public/app/services/routes.js) to download the correct view (a file in the public/app/views directory) and marry it to a controller (a JavaScript file in the public/app/controllers directory).
All the displayed values are data bound either to a controller (e.g., orderProductControler
) or to a Breeze entity exposed by the controller (e.g., Product
)
BreezeJS on the client
Breeze handles the application's data modeling and data access chores. "Entities" are JavaScript types constructed by a combination of developer code (in public/app/services/model.js) and Breeze metadata (in public/app/metadata.js).
The Breeze EntityManager
and related components handle the details of creating, querying, materializing, caching, change-tracking, validating, and saving entity data.
While the controllers could interact directly with the Breeze EntityManager
we prefer that controllers make data requests through a facade, here called the dataservice
(public/app/services/dataservice). The dataservice
provides the controllers with a higher level data abstraction, shielding them from application-specific details of working with Breeze.
The Zza dataservice API offers kind of looks like this:
{ initialize, saveChanges, cartOrder, draftOrder, orderStatuses, products, productOptions, productSizes }
That's what we mean by "higher level data abstraction". If it reminds you of the Unit-of-Work and Repository patterns ... then you've caught on to what we have in mind.
The Zza! client is server agnostic
The client-side code is virtually unaware of the server technology. Everything you've learned about programming in Angular and Breeze applies without alteration.
To prove that point, the exact same Zza! client - HTML, JavaScript and CSS - runs in front of a .NET server, hosted on IIS, running the Web API, Entity Framework, and SQL Server.
Exactly four files are different:
metadata.js, reflecting the difference between a relational schema and MongoDB document schema.
model.js, for much the same reason
environment.js, a small configuration file
/scripts/breeze.dataservice.mongo.js, a Breeze "dataservice adapter" plug-in that handles low level details of communicating with node and a MongoDB server.
Otherwise, the same client-code runs in radically different environments.
Node and Express
The web application server is written in about 60 lines of JavaScript (server.js), using the Express framework running on the node.js platform.
This server is either delivering static content to the client such as HTML, CSS, image, and JavaScript files or routing client data requests to a data access module called routes.js.
Breeze on the Server
The server-side data management component (routes.js) is developer-written, application-specific code for handling client data requests such as queries and saves.
Here you decide what data request your server will honor and implement the necessary business logic including validations.
This particular Zza! application server handles a small number of requests.
Most are so-called "namedQueries":
var namedQuery = { customers: makeVanillaCollectionQuery('Customer'), orders: makeVanillaCollectionQuery('Order'), orderstatuses: makeVanillaCollectionQuery('OrderStatus'), products: makeVanillaCollectionQuery('Product'), productoptions: makeVanillaCollectionQuery('ProductOption'), productsizes: makeVanillaCollectionQuery('ProductSize'), lookups: lookups };
These are Breeze-enabled query endpoints that understand the OData query semantics familiar to Breeze client-side developers. For example, a Breeze client query such as:
EntityQuery.from('products') .where ('name', 'gt', 'Caesar') // products whose name comes after `Caesar` .orderBy('name') // sort by product name .skip(5).take(5) // skip the first and take the second (pagesize = 5) .using(manager).execute();
becomes a GET request like this one:
http://localhost:3000/breeze/zza/products?$filter=name gt 'Caesar'&$orderby=name&$skip=5&$top=5
and returns a page worth of JSON product data such as this:
[ { _id: 31, type: "drink", name: "Cola", description: "Cola", image: "cola.jpg", hasOptions: false, isPremium: false, isVegetarian: false, withTomatoSauce: false, sizeIds: [ 10, 11, 12 ] }, {...}, {...}, {...}, {...} ]
Plain old GET endpoints
This OData query facility is made possible thanks to the breeze-mongodb module that you installed with npm.
Your server doesn't have to support OData queries. You can simply return an object and let the Breeze client sort it out.
The lookups
endpoint does just that. Its response to the client is a single object whose properties are arrays of the four reference entities (OrderStatus
, Product
, ProductOption
, ProductSize
):
function lookups(req, res, next) { var lookups = {}; var queryCountDown = 0; var done = processResults(res, next); getAll('OrderStatus','OrderStatus'); getAll('Product','Product'); getAll('ProductOption','ProductOption'); getAll('ProductSize','ProductSize'); function getAll(collectionName, entityType) { db.collection(collectionName, {strict: true} , function (err, collection) {...} } }
With this one call, the client receives all of the reference items it needs to populate comboboxes and drive pricing calculations. The Breeze client automatically shreds the package and stows these entity lists in cache.
It bears repeating that the "magic" in this call is all on the client. The lookups
implementation is standard JavaScript with four standard MongoDB queries in the inner getAll
function.
Learn more here about the breeze-mongodb module and programming with breeze, node, and MongoDB.
The Zza! Mongo Database
The Zza! MongoDB database is pre-loaded with ample sample data.
The Customer and Order... stuff are the transactional data representing the activity of the Zza! pizza parlor.
In a relational database, the Order, OrderItems and OrderItemOptions would be in separate tables, related via foreign keys.
This is a Mongo database with "collections" instead of "tables" and "documents" instead of "rows". A document can have nested sub-documents.
In this database, there is an Orders collection but no OrderItems or OrderItemOptions collections. Instead, each Order has an "items" array of OrderItem
sub-documents. Each OrderItem
sub-document has an "options" array of OrderItemOption
sub-documents.
Here's an illustration with a sample data snapshot in the background:
Breeze metadata can describe complex documents like the Zza! Order document. Root documents - the objects like Order
in MongoDB collections - map to Breeze EntityTypes
. The sub-documents, which have no existence independent of their parent documents, map to Breeze ComplexTypes
.
Breeze already supports scalar (single valued) complex types for relational databases. Relational databases don't have collection properties. Document databases do. So Breeze supports arrays of complex types.
Schema summary
The OrderStatuses, Products, ProductOptions, and ProductSizes are relatively static reference data. The application can't change them. Each has a document in its own MongoDB collection.
So the entire Zza! database consists of seven collections:
Order
OrderStatus
Product
ProductOption
ProductSize
On the backlog
This sample is a work in progress. Among the features yet to be implemented:
- Place the order (and save it to the database)
- Client- and server-side validation
- Query and review past orders (and those of other customers)
- Deliver a delicious, hot pizza to your laptop
Breeze is ready to tackle the first three; we just need a little more time to work on the code. We haven't figured out the pizza delivery system yet. We're thinking about it.