What do you want to learn?
Skip to main content
Building a Realtime Web Application with Meteor.js
by Rob Conery
Go beyond a slick demo, and build a usable eCommerce site with Meteor.js
Start CourseBookmarkAdd to Channel
Table of contents
Getting Your Head Around Meteor.js
Cutting to the Chase
Generating the Site
Let's get off the ground by prototyping our application. In this module we'll generate the site and get the initial formatting down. Then we'll add in some routing. Next we'll add a Homepage and we'll begin working with data. Then we'll create a product page and, finally, deploy to Meteor.com for review. Let's get started installing Meteor.js. I'm here at Meteor.com and here, once again, you can read over the documentation and you can take a look at some of the marketing stuff down below. If you click on Install Meteor you can see that is a simple curl command and it's going to download an installation script and it takes about 10 or so seconds to get the whole Meteor platform installed. I already have it installed, so if I type in Meteor here at the command line you can see it says, you're not in a Meteor project directory, you have to give it a command, and if you want to know what those commands are you can type in meteor help using a --help. That's always a good thing to do because then you can take a look at all the commands at your disposal and in here you can see we have plenty of them and we'll go over them, some of them, later on. The big one that I want to play with now is create because that is going to create our site for us using a generator and it's going to create a directory and drop in some initial files, so if I change directories into rocket-shop we can see our brand new site, and all we have to do is run Meteor. It'll sense that we're in a Meteor directory and it'll start the server, startup MongoDB, and we'll be good to go, so let's take a look at localhost:3000. Yay! Welcome to Meteor! Not a terribly motivating or exciting experience, but they do show you this Click Me button, and if you click it you can see a counter go up and I suppose that's exciting, but let's just kind of scoot right on past that. What I want to do next is I want to install the Bootstrap starter template, as well as I'm going to be using Bootsnipp for a lot of the HTML templates that I'm going to be using later on, so I want to plum those things in straightaway, so let's take a look at what's in our directory right now. We have rocket-shop.css, rocket-shop.html, rocket-shop.js. If you've played with Meteor before, which I hope you have, then you know that these things are not anything that we want. What we do want to do right now is we want to actually structure this in a better way, more than just having a demo. I'm going to create a set of directories and these all have meaning to a Meteor application, server, client, public, and lib, and in addition I'm also going to add a settings.json file. We'll talk more about those later on, but I want to have two of them, settings.development.json and settings.json. These will hold various application settings that we'll need later on. For now let's open up web storm and I'll do that from the command line, and take a look at what we have as far as a basic skeleton of our site. What I want to show you here is a traditional kind of standard directory for working with Meteor, the other .meteor directory that holds all the magic. We're not going to worry about what's in there. We have client, lib, public, and server. In addition, we have settings for development and settings for production and just the regular old settings.json page. Let's take a closer look at the other directories, and we'll start with the lib directory and this is important. Lib can be accessed from everywhere and if you've used Meteor before you know that you have code that you write that could be accessed in the client, the browser, as well as the server. That code goes in lib. If you only want code on the server, then you put it in the server directory. If you only want code on the client, well, you put it in the client directory. The public directory is for images and other assets, not necessarily style sheets, you put style sheets separately, they get collected and compiled by Meteor. I should say if you don't know what I'm talking about right now, well, you might want to go watch John Sonmez's video. He talks about all of these things. However, if you're the kind of person that can easily pick things up as you go along, and you don't mind a little frustration, well then hang tight, we'll talk about all this stuff, not necessarily now though, so fair warning. What code you put where is quite important and not only that, the name of the file is important as well, if you're depending on load order, in other words, what code gets loaded when. Hopefully you're not worried about that because if you are, then Meteor can cause problems for you. We'll see that later on and we'll also talk about load order a little bit more later on. Just know for now that anything in the lib directory will be loaded first, then comes server, then comes client. Files inside those directories then get loaded in alphabetical order and that includes their full path, so if you have subdirectories in there, well those subdirectories are going to be evaluated when loading everything in alphabetical order, so the bottom line, outside of the lib loading first, and then server, and then client, don't depend on load order. That actually can come back to bit you. We're going to tango with it later on when we try and do some namespacing gymnastics, but we'll get there. For now let's fill out our client directory here. What I want to do is I want to make sure I have a specific directory for templates because templates are going to be the things that are rendered out to our users. The next thing I want to do is create a directory for style sheets. As I mentioned, Meteor will take all the style sheets for the client and it will compress them and concatenate them down for you, which is incredibly handy. In addition, we'll be using some third party scripts like Knockout and Accounting and Moment.js, so I'm going to put that in a scripts directory here in client. Then I want to have a special app directory. In this directory I want to put things like my layout, so let's add that right now, and I'll just create a file and I'll call it layout.html and I'll drop it straight in and it'll be empty for a second, but let's jump back over to Chrome, and I'm going to copy and paste the Bootstrap starter template here, I'm just going to copy it straight from the page and drop it in. There we go. I have a few links here that I have to change and I'll do that in a second, but right now what I want to do is I want to show you something interesting about Meteor. Let's startup our server again, and when we do notice that we get an error. It's saying in your app layout.html file you can't set the doc type here, Meteor sets it for you. Now if you are familiar with Meteor, then you know all about this and the shadow DOM, but if you are not familiar check this out. Meteor has a shadow DOM that it creates kind of in the background, in memory if you will, and it takes all your HTML files and injects them into that shadow DOM and then renders them out to the page. Refreshing, there's our starter template and it handled all the doc type for us and everything, that's kind of nice, and if we take a look at the page source here you can see what a Meteor app really is. There is our doc type tag. Right here we have a big fat dump from the Meteor runtime and it's the runtime configuration. Reminds me a lot of asp.net. That's pretty funny, the post back, and then down below that a bunch of script tags with all the individual packages that make Meteor run, including some of our client code. Finally, down below here, well that's our Bootstrap styly stuff and it's all broken, doesn't look very good. Our next task, let's put some HTML in this screen and make things look a little bit better.
Initial Route Templates
Well our site is probably going to have more than one page and even though this is a prototype it's important to show navigation and well basically different pages other than the homepage, so let's do that now. I will create some additional routes here like about and I will put them all in the home directory, so that means I need to name them homeAbout, and the other one I'll create is one for contacts because, well, we're going to have a contact page as well, and I'll name this homeContact, again, camel-cased as you've seen before. Following the structure that I have I could just put about and contact right underneath the home directory. I like this kind of organization, it reminds me a lot of Rails, and it's something that I'm very used to. In here I have to make sure the name is homeAbout or else the router won't be able to find it and it won't know what's going on and I'll add an additional file here, contact.html, and same thing, I'll rename this to be homeContact. For now let's just stub out the title of the page and I'll put in some first level headers here and I'll just put in Contact here and then heading over to eth about page and I will put in another first level header and I'll just put About. All right, well let's go and check our work. If we flip back over to the browser and I type in the URL, let's do /about, there it is, the About page, and then I can do the same thing with Contact, just to make sure it'll show up with the routes, and there's Contact. Excellent. That's nice, but I'd rather have links up here, so users don't have to guess and type in the URL. Let's flip back over into our templates here and what I'll do is I will go over to our nav partial and down below you can see there is the about and the contact links. What I can do to navigate is I can use this navigation helper, pathFor, that's from iron router, and I can just say pathFor 'about', there we go, and I can put 'contact' down below here, and then up here I'll put index, but let me ask you, does this look right to you? Does it make sense pathfor 'index', 'about', and 'contact'? Hmm. How would the router even know what I'm talking about if this is a router helper? Let's go back over to the router and check things out. Here I have routes named homeAbout and homeContacts and homeIndex, hmm, but the URL goes to about and contact, yeah it doesn't make sense, and indeed, that should be the name of the route not the URL that I want to go to. A very very simple error to make and one that's all too common, so here we go, homeIndex, homeAbout, homeContact, that's what I want to see, and clicking on these guys, there you go. Notice also that I don't have a pound sign in the URL. This is because Meteor leans on push state, now with the browser's history API. that's pretty neat. It'll gracefully degrade if it sees older browsers, but since I have a newer browser it's just making it look as if it's a normal URL. The last thing I need to do here is to remove project name from the navbar. Let's put in The Rocket Shop and then also add a Home path and there we go, this is looking pretty good.
Well this might seem like a lot of fluff to a lot a lot of web developers out there, but having a Favicon is huge. I went over to the Favicon and App Icon Generator and loaded up our logo and it gave me a bunch of Favicons, as well as some HTML to use, and I want to put that in right now. I think this is actually a great bit of embellishment, if you will, that is going to make your stakeholders extremely happy because it's going to make the app look real, so let's add an images directory to our public directory and in here I'll load up all of the images that are in the downloads for this course. We have icons, products, splash, vendors, and some credit card stuff down below. Specifically what I'm interested in though is the manifest and icon stuff that the icon generator gave to us. Alright, well how to I actually put these in? Well I've created a favicons.html page in our template and basically I copied and pasted all of the HTML that was given to us by the generator and I reset the links, as you can see here and I just put them inside of a head tag. That's right, another head tag. This is going to be injected right underneath our title. I think this is pretty cool. This is one of the neat things about working with a shadow DOM in Meteor. Alright, well let's head back over to our page and make sure that this works, and if I refresh boom, there's our little rocket. That's kind of neat. If we take a look at the page source and scroll down it is kind of as you would expect there is the favicon stuff in terms of the links and then underneath that you can see the title.
What we have on our homepage right now is Welcome to the Rocket Shop and we kind of have to offer a little bit more that users that come along because, well, a greeting is nice, but we should probably have more information. The first thing I'm going to do is to drop in some ready HTML. You can grab this from the downloads for this course or if you don't have access to the downloads for this course you can go to the GitHub URL that you see on the bottom of your screen. The HTML for the homepage here is fairly traditional. We have a big old splash at the top and then some features products down at the bottom and to see this I just can't resist, I've got to drag this thing over and I haven't saved it yet, but I just want to show you this one more time because I love it. I'll save it and boom, I love how Meteor just refreshes and shows the latest changes, that's pretty neat, but if you look at the layout here it's kind of stinky. What I'm going to do is I'm going to add some styling specifically for the homepage and I'm going to do this under stylesheets, I'm going to call it home.css. Remember that Meteor takes all of the CSS files that it finds inside the client directory and it will smash smash it together, concatenate, and drop it on the page for you and that still doesn't look so good does it? We clearly have a problem and I think I might know what it is. Let's take a look. I'll stretch this out a little bit, yeah, you can see we have some serious problems. This is supposed to stretch all the way to the end and I'm guessing we have a problem with our layout and our template, so one thing we can do is we can just go to Inspect Element and take a look at the way things look here. There's our cover, oop, we have two containers. We have an outer container and an inner container and this is a big deal when working with a framework like Bootstrap or Foundation, you really need to understand the wrappers and the page wrappers and where they are, especially when you're parsing things like I am, and here I put a container for everything outside of our yield. I don't want to do that. I want to make sure every page has the freedom to format itself and there you go, I remove it, and look at that, it refreshes itself instantly, I love that, and it looks great. Well, except for the little product boxes down below here, I'm going to need to add some styling for that, and I haven't added it just yet, so let's add another CSS page, and I'll call this one product-tile.css. I really like adding the targeted CSS into individual files here that then get crunched down, minified, concatenated, and so on later on and when I save that, boom, it is applied, and that looks pretty good. All we're missing now are the images that go along with these tiles, I'll add that next.
We have our groovy little product tiles here, but we don't' really have a product page that we can view the details with, so let's add that now, and I'll go at a little bit of a faster pace here because this is essentially review. The first thing I'm going to do is to add a route and it's going to be products and this time I'm going to specify a parameter using :sku, this is traditional routing kind of syntax if you will. I'm also going to pass in some data and data can be an object or a function. In this case, I need it to be a function, and that function is going to return, now in this case I'm going to have to loop over products and I'm going to need to find the product with a given sku. Find is an underscore function and it'll return the very first item in an array that matches a given condition, so for this I can pull the sku out of the parameters using this.params.sku. Now this works, but I'd rather help myself in the future because I know I'm going to be working with Mongo because that's what backs Meteor, so I'm going to create a method called Products.findOne, and well Products.findOne is going to do the exact same thing for now. Later on I'll take this out when I plug in Mongo. This is going to take some arguments and inside of here I'll just ask for args.sku. Perfect. That means I can go back into my route here, again, helping myself in the future, and I can ask for Products.findOne where sku is equal to this.params.sku. Okay, well next I need to make sure that I have an actual template. Right now I've specified the name product, but I'm going to change that in just a bit, so inside of here I'm going to have products and then show.html. I'm following a Rails convention here where if I want to see an individual model or item, then I'm going to use show, and so inside of show I will dump in productsShow. Again, naming your template is very very important because you're going to want to be able to match a URL against a directory structure, so it makes sense to you later on in the future or with people that come and try and pickup what's going on. Okay, inside the products page here I'm going to add some ready HTML that you can, once again, find in the downloads for this course or you can go and grab the code up at GitHub using the URL I showed you in the few clips ago. Now that we have a router and a template let's add a link. Over here in my productsTile template I can now do a path for productsShow, but the route specifies a parameter, and I haven't specified one here, why not? Well the short answer is that pathFor looks at this to see if it has any property that matches a parameter on the route that you're trying to go for. If it does, well it'll construct it for you using this. Notice, if I hover over more details it does exactly that, it finds a sku on the product, and is able to create the URL for me. Very handy. Okay, so we're heading over to the page, it works, sort of, looks like the formatting's a little bit off, so let's go and fix that. Heading over to my productShow template and yeah, the image isn't set, so let's set that now. We'll do images/products and then image and I can set all this directly because the data is being handed to me directly from the route. I'm not using a helper this time. Well, oh, it looks okay, but it looks like I have some formatting problems. Let's readjust this a little bit. Wrong way Rob, let's give this seven columns by five columns, there we go, that looks much better, and the price looks good too. I'm using the money helper. Man that makes me happy being able to use accounting just like that so easily, but we have another problem. Looks like I am expecting some markdown on this page and hmm, it's not being converted. Let's do that now. I could create a brand new helper and I'm going to just call it markdown and this is going to use showdown and for this I have to use two lines of code, darn, and this is going to create a brand new showdown converter and it's going to return makeHtml, which is going to take the markdown and turn it into HTML for me, and then I can plug that in using markdown just right here in line. Refreshing in that almost worked. It's encoding the HTML for us, which is nice of it. What we need to do is to make this unsafe by using three braces and when I do that look at that, the markdown is working on our product page nicely.
Deploy to Meteor
I'm proud of our little site. How about you? We've done a lot in a very short amount of time. Let's show the world. If we type in meteor--help there's a number of commands in here and there's two to note. The first is login and as it turns out you can have a meteor.com account, which I do, you can see my email address here. It takes a second to sign up, no money is required, you can actually balance authentication off of this thing too, so yeah, go and get yourself a meteor.com account because one of the cool things that you can do is actually deploy your site to meteor.com. Now this isn't for production use, this is more for, hey world, look at my demo, look at the package I made, client look at what I've made for you. This is neat stuff. It's public, you can't lock it down, you should know that, but if you have work you want to share, like a demo, this is great, so if we want to deploy our site up to show our team or other people we can just say meteor deploy and we'll give it a name. In this case, it'll be rocket-shop. Now notice the deployment process for Meteor, this is interesting. It's sort of built-in. It minifies and builds your site ready for production. It builds this big old slug if you will and drops it down on disk and then it'll send it off. We'll see this more later on when we use meteor up, but right now we have our site up and ready at rocket-shop.meteor.com, and we can go there and take a look at what we have just built. Heading over here to the browser, dropping it in. I should mention this does take a while to actually get up and running, a while meaning like a minute or two, not like an hour, but as you notice, I'm missing some graphics, but as I said, it does take a second to go live. If I refresh this thing right now, then boom, there we go, we have our site up. We can then send an email to our client and say, hey look, here's the prototype. Now again, this is not private, so if it's a sensitive site, by all means, you're going to want to have your own server and we will discuss that in module seven. In this module we kicked up a prototype, so we could get off the ground quickly flexing a lot of Meteor's goodness. Then we implemented routing, layouts, and partials. After that we added a home page and stubbed out some test data. Finally, we put everything up for review at meteor.com.
In this module, we're going to plug in authentication. Specifically we're going to run through a basic setup and installation of all the authentication parts. Then we'll configure it to meet our needs. Next, we'll add external services like GitHub and Google. Finally, we'll augment our membership system by adding role-based authorization. Our application needs membership, and hopefully by now you're starting to see that if you need something, it's always good to go check out atmosphere.js and see what they have up there. So plugging in membership didn't really do much, but if I look at accounts, we'll take a look at what pops up, yeah, there is a ton of account stuff, and in fact, it is kind of already built into Meteor, and it's really, really thorough, I'll just tell you that right now. So we have accounts-password functionality, we have accounts-ui, we have stuff specific to Twitter Bootstrap as you see here, it just plugs right into it. Installation is fairly trivial, do again, meteor add and then ian:accounts-ui-bootstrap-3, and then using it. If we take a look down below, all we've got to do is drop in this login button's partial, and as you can see, it goes right up there in your navbar; you just need to add another unordered list. We can do that. So, let's start out by adding the package ian:accounts-ui-bootstrap-3. Again, this is only the account's UI stuff, this is not the account's itself. As you can see, it's added the accounts-base, because that's a dependency, but it's not going to give us much more functionality other than that. Okay, well, heading over to nav.html, which is our nav partial, I'll just paste in the code from the atmosphere.js page, and it's going to output the login button's partial right here, but it's going to be pulled over to the right. And refreshing, I don't see anything-- I sort of do. If I highlight up in the top right, it says no login services configured. Well that's because when I installed this thing, all we got was accounts-base. That just means, hey, here's the tables and some models and other things, but it doesn't really specify what specific types of accounts I want. So for that, I need to add accounts-password, meaning users can log in using a username and password. You don't have to have that, you can have other services too if you like. Okay, now that that's in here, let's go and take a look and make sure that it was installed, and it was, we have email, bcrypt, sha, and so on, and boom, our UI package has updated itself. Look at that! Neat little dropdown for logging in and also creating an account, so let's do that. I'll just drop in my email address, drop in a password, and create an account. And I'm logged in. That's pretty easy stuff right there. I can open up the console down below, and here I can use a special function called Meteor.user, and there you can see that it's actually a function. Let's pull this up a little bit here. If I want to take a look at the logged in user, I just say Meteor.user and invoke it. There's my ID, I've been saved. I have an email in here. I also have a profile that I can add some interesting information to.
This accounts package from Meteor is not a toy, it is actually quite capable. If you head over to docs.meteor.com, take a look at the account's API where can see all of the different things that you can do with it. You can get the current user, you can get the current user ID, you can take a look at all the users if you want. It's got permissions on top of it, it's got all kinds of restrictions, so the current user can't elevate or change any of their information. And if you take a look at this right here, you have different services that you can plug in for a given account, including Facebook, Twitter, Google, and so on. We'll look at that in the very next clip. What I want to do here is I want to change the default configuration a little bit. And you can see some of the options that I have available to me, including send verification email. I really like that. It'll send the email for you and then you've just got to click on, and then our account is verified. I can restrict things by domain, I can forbid creation if I want. In this config, option is available anywhere, but specifically what I want is something to restrict only on the client, meaning I want to update our UI so that it asks for more than just an email, password, I also want a username. Now you can see you've got a bunch of options here, but for right now I want to have just passwordSignupFields to be USERNAME_AND_EMAIL. I have a couple of choices here and I'm not a fan of the all caps string magic here, but that's just the way it is, USERNAME_AND_EMAIL. And you can just drop that into Accounts.ui.config on the client, and that's the important thing. If you put it on the server, you're going to get an error. You might be saying, well, how would I know that? Well, as I showed you before up on the top right, you have the option they tell you if it's client, server, or both. Anyway, here I'm going to drop accounts.js into a brand-new directory called config, and so inside of here I will put Accounts.ui.config, and I will drop in that switch passwordSignupFields USERNAME_AND_EMAIL all caps magical string, hooray! All right, fair enough. So let's go back over to our shop, and I'll refresh, and it takes just a second. So here let's sign out and I want to sign in one more time, and you can see Username or Email is now the prompt for signing in. I like that. But for creating an account, you have three fields here. So I could say robconery and then my email and then my password. This is a good idea in case your users what to change email, it doesn't really matter, it's not dependent on their login. Some people like having a username, including myself.
Github and Google
Let's take a minute and play around here with our brand-new authentication scheme. I can now access Meteor.user as you can see here, and that returns a user's record with an ID, email, profile, username, and various services. And I can also access all the users. As you can see, if I type in Meteor.users, this is a Mongo collection and I have certain things I can do here. So, if it's a Mongo collection, I should be able to query it like a Mongo collection, and indeed I can. I can just say Meteor.users.find, and then I have to call .fetch. And you can see I have four users in the system currently. Now obviously that is a security problem. We'll take care of this in just a little bit. For right now what I want to do is I want to be able to work with a concept with roles. So I want to be able to set myself as an administrator in the system. Now, obviously you don't want all your users to be able to do this, but for right now, I just want to show how this works. I can run an update command right here from the console, and I'll just issue a set to Mongo, and I'll just say set profile.name equal to my full name, Rob Conery, and watch this, this is interesting. In the top right where you see my username, right here, keep an eye on that, because once I hit Enter, boom, I love that. So from this we know that Meteor.users is pub/sub aware, it's reactive, so any changes made on the back-end will be reflected on the front-end and that the account's UI and so on will favor a profile.name. All right, what if we want to now update roles to be administrator? We don't have this in the system now, but if we wanted to add it just by saving, we can't. Access is denied. The reason I wanted to show you this is that the accounts package locks down the Meteor.users collection, so that you can only update a few things, and one of the things that you can update is your profile, but you can't just add ad-hoc information. But since we're developers and hackers, we can go into the Mongo instance backing our Meteor app. As long as Meteor's running, we can go open up a brand-new tab here and say meteor mongo, and that's going to open up our MongoDB instance. And here I can do the same query, Meteor.users.update, and you can see I added the administrator role. Can we access this from the front-end? Well, if I ask for the Meteor user, which is me, the current user, no. You'll notice that I don't have any roles in here. And the reason why, well, it is because again, the accounts package locks down what is given down to the client, so I don't have any roles returned. That's okay, because what I should be doing anyway is searching for a package for roles, and sure enough, there's one by alanning, as you can see here, and this offers some helpers, it augments Meteor.user, and as you can see here, we can add a user to a role, we can check if the user is in a role. We also have some Blaze helpers that we can then ask if isInRole, and then pass off admin or administrator, and then output some information on the screen. And that, as it turns out, is precisely what I want to do. So right here next to login buttons, let's add another list item. And here I will just use that if statement that's right there on the atmosphere page, isInRole admin, and then I'll drop out here a link to whatever the administration page could be. So for now I'll just put in a pound sign, because we don't have an administration page, and it'll just say Admin. All right, then I need to add the package here, add alanning:roles, and that will go in, here we go, and let's check and make sure our server has restarted itself. It has, and I'm getting prompted to turn off auto publish-- we'll talk about that later on. It's a good thing to have Meteor tell us that our site's not safe. By the way, again, we'll talk about auto publish soon enough, but just know this, Meteor's got our back. Okay, so heading back over here, hmm, refreshing. I should see that admin link, because I have roles set to administrator. Yeah, that's the problem. It was Admin. Here we go, Administrator. And if I refresh, it should see that array. And it does, and I am in as admin. Pretty easy stuff. In this module, we plugged in membership, and we did a basic setup and installation of the membership parts and we configured it to make sure we get a username and a password, as well as an email. Then we added some external services, specifically GitHub and Google, but there are more, like Facebook and Twitter. And finally we added role-based authorization using the alanning roles package.
Prototyping is over and now it's time to build out the core of our application. So, in this module, we are going to use our very first collection. We're going to restrict fields and access to that collection, so people can't just save things willy-nilly. Then we're going to do it all again, but this time at top speed. Then, finally we'll take a look at ORMs and some issues that you might run into when you're trying to namespace all of the bits of your application. We do have data, but it's just data coming out of a JSON file pretty much. We need to get this stuff into the database, but there are a number of considerations that we need to think about, because, well, security is a big deal. And if you remember reading about Meteor on Hacker News a few years ago, I think it was in 2012, they had a lot of mixed reactions. A lot of people were jumping up and down, saying, oh my gosh, this is amazing, and other people were saying, oh my gosh, this is a joke! There was no security, and if you had any data in a database, people from the client could just reach right in and delete it, change it, tweak it, do whatever. The deal is, the Meteor team was just trying to show a proof of concept, and the poor guys, well, that's what happens when you push too early. So Meteor kind of got a bad name. Well, nowadays, this is really important to know, nowadays a lot of security is in place, so at the discovermeteor.com blog, as you're seeing here, a lot of great article including setting the record straight on how you can do things without getting into trouble with security and Meteor. It's very important to know these things, and because Meteor basically shouts at you, as you've been seeing, turn off autopublish, turn off insecure, we're going to do those things, I just want you to know that, but what you're about to see is going to look, well, not so secure. So let's just jump right to it; know that we're going to tweak it. Okay, the first thing I'm going to do here is I'm going to rename this big old dump right here to product seeds, and I'm going to set it to be a variable. Then I'm going to declare a brand-new collection, and this is how you do it. You drop it in the lib directory; I'm going to put it into a collections subdirectory, and I'm going to see if we have products in the database. Now notice that since this is in the lib directory, it is accessible by both the client and the server, and therein lies the rub. You can query straight from the client in the same way you can query from the server, and you need that to be secure. All right. Well, what I'm doing here is I am checking to see if there are any products in the database, and if there aren't, then I'm going to loop over the product seeds and jam them in. But I don't want those to be here next to my collection. I'd rather have it be server only, so I'm going to put it in a seeds.js file here under the server directory, so this is server only, which is good. And then down below here, I just checked to be sure that if there's no products that I'm going to loop over and add all the products in, and as you can see, just saving that file restarted Meteor, and our products are in Mongo. It's just that easy. And that's one thing I really like, as long as you're paying attention to all the details and security stuff, and we will be. Okay, so for Products.featured, I am just going to now return a Mongo query. I'm going to take out the find, because we're no longer working with JSON. If I did everything right, then I should be able to now query all of the products down below here in the console, and I can. This is coming straight out of MongoDB. Our homepage should look exactly as it should, and down below here, yep, that transition was easy. One of the reasons it was easy is because I helped myself in the future. If you recall before, I added some methods to products, specifically findOne and featured, because I knew I was going to be working with Mongo later on. I was able to just remove the findOne method entirely, and with the featured method, I just had to replace one line of code.
Well, our boss just dropped by and said we have to have a couple more fields here in the database, specifically we need to have some cost information, and also some inventory numbers that needs to go straight in, so I have updated the seeds, as you can see here. However, that leaves me with a bit of a problem. I don't want to show that stuff. For now, let's reload the database, and I can do that with meteor reset. That clears out Mongo. And now if I just start the application again, it's going to see there's no products in the products collection, and it's going to jam in those products; as you can see, it did. Now let's flip back over to the browser, and I'll open up the console down below. And let's query our products and see what kind of information we're getting back. So I can do that by using Products.featured, and whoops, I have to say featured.fetch. There we go. And that'll fetch the actual records. And yeah, whoops, there is cost and there is inventory. I don't want that information sent down to the client. So how can I actually restrict this? Well, Meteor has a few ways that you can restrict access to your collection. There is allow and there is deny, but those are specific to updating and inserting and removal operations, and that's not going to help me. What I need to do is I need to restrict the fields that come back, and I can do that per query. And this gets a little bit cumbersome, because every query you have for products and it needs to do this, but basically I can specify an array for fields and then I can say I don't want inventory and I don't want cost. That's one way of doing it. Flipping back over to our browser, and let's try Products.featured.fetch one more time, and there is our product. And if we scroll down, cost is gone, and inventory is gone. This works well; the problem is, of course, I have to have that field specification for every product's query, but that's okay, I can live with it. We actually have a bigger problem, though. Take a look at the price for the one time reentry to Earth, it's $83,000. Well, what happens if someone realizes we're running a Meteor site, and they say, hmm, I wonder if there's a products collection, and they figure out, yes there is, and I wonder what would happen if I ran an update query from a client right here where I was able to set the price of that ID to 0. Well, look at that. Could you imagine if that happened in production? I'm sure it's out there somewhere. Nothing says fire me please than an oversight like this. So, of course, we need to take care of that. We don't want that ability for our users, because they need to actually pay for these products. We'll take care of that next.
Having all kinds of fun resetting the price on this product here. I'm sure my boss wouldn't really like that. Anyway, let's go and change it back, 123, that's fine. I want to remind you that, yes, Meteor's had security issues in the past. In fact, there's a great quote here in this article, and it's quoted, "inherent security issues." And it depends on how you look at it. Yes, Meteor is wide open when you install it and create a brand-new application; they do that because it's easier for you to get off the ground. But they fully expect you to change things and lock things down and specify what users are allowed to do and what they're not allowed to do, and for that, you have allow and deny methods on your collections. I'm not going to dive too deeply into this. John covers this in his Meteor Fundamentals course, and I highly suggest you take a look at that, but for now, we can play around here with the allow function. First thing I want to do is I want to have a permissions file, and that permissions file is going to be on both the client and the server, so I'm going to put it in my lib directory. I'm going to call it permissions.js. I'm going to create a function called isAdmin, and if you're saying, dude, that's global, I'm going to say, dude, you're right, because that's just the name of the game when it comes to Meteor. I'll have more to say about that later on. Anyway, let's take a look at whether the user's logged in and then whether they're in a specific role. In this case, I want to check and see if they're in the administrator role. So, flipping back over to the browser, let's make sure we have access to this function, and I can see it, there it is. If I run it, nope, it's false, I'm not an administrator. But, we also have another problem, and since I reset the database, I don't have any users now. So, let's fix that, but what I want to do here is I want to actually add a seed rather than having to go log in every single time. So, down below here, I will just add users and same check, if Meteor.users.find.count is equal to 0, then I'm going to go ahead and drop in a brand-new user. And that's how you do it. Acounts.createUser. And specify the username, and email@example.com, password, profile. I'm also going to create a roles key here, and I'm going to set it to an empty array. Finally down below there, I'm going to use the roles package with Roles.addUsersToRoles, and sending it to admin, which really should be administrator, there we go. Okay, so what I need to do now then is I need to be sure to do a Meteor reset, because I do have products, and I just want a nice clean database. You'll do this fairly often when you're developing out your site. And it's a good thing to do to have seeds and make sure you have good test data in there. All right, now building the application, and good, it inserted our products and it added the Admin user. Excellent. That's what we want to see. So let's flip back over to the browser and if I refresh and go to sign in, you can see we've lost our configurations for GitHub and Google; those are stored in the database, and since I wiped the database, I lost my configuration, which is a bummer. Admin@test.com, admin123 as my password, signing in, and there I am, Big Admin, and my role is picked up as well. Perfect, that's exactly what I want to see. So, if I check isAdmin, I get true back. Again, that's also what I want to see. All right, so this is a tangential, kind of roundabout way of saying we can now go and start working with Products.allow. Products.allow takes a single argument, which should be an object with three keys, and those three keys should be update, insert, or remove, and you don't have to have them all, just the ones you want. Anyway, here I'm specifying that update should only be allowed if the user is admin. Now notice here that I can also interrogate the user ID or the specific product that is going to be updated. Insert works exactly the same way. So I might want to check status, maybe you don't want to allow people to save product that is released, who knows, up to you. Anyway, here I'm just returning whether the user is admin. I can use this here on the server, because once again, our permissions file is in the lib directory, so isAdmin is available in the client and the server. All right, so let's pull back Products.featured, and I'm going to grab the ID off of our one-way-reentry one more time, and I have to do this, because again, I just reset the database. And so let's rerun that update query and see what happens. I'm not logged in, but I do want to see if I can change the price on that, see if I can get it back down to 0, because you know it is pretty expensive, and it's kind of taking advantage of people that want to get back to Earth real fast, don't you think? I'm just trying to help out here, that's really all I'm trying to do. Okay, setting the price to 0, and nope. But did you notice that it changed? It changed on the client, but with a latency compensation it came back and said, no, and it reset it once again to the price that's in the database. This is a critical thing to understand. This is the latency compensation in action. We're going to see this again later on, but look at that, it flashed to 0, and then changed back again. Okay, let me log in, and I'm going to log in as firstname.lastname@example.org, and admin123 as my password. Signing in now I have administrative privileges. I have successfully hacked into the system and now I can set the price to 0 and that's as it should be. You should be able to come back to Earth at any time that you like.
The Insecure Package
As I mentioned in a previous clip, Meteor comes out of the box with two helper packages that kind of allow you to focus on the business goal of your site, get the prototype up and running, and then as you build things out, you're probably going to want to remove them. The first one is insecure, and as you can see the description here, is allow the all database writes by default. That means from the client you can play with the database and goof around. That can actually be really helpful when you're developing, but you don't want to forget about that. The second one is autopublish, which publishes everything, every collection that's in the database that publishes it down to the client. We'll talk about publish and subscribe in a later module. I'm going to leave autopublish in place right now, because I don't want to have to think about subscriptions just yet, but I just can't allow myself to have insecure be in place, it just gives me the willies. So, I'm going to remove insecure right now. And if we go back over and see if that worked, I want to check two things here. The first is, good, access is denied, that's as it should be. The second thing I want to make sure of is that my allow bits will allow me as the administrator to change the data so I can sign in and notice I am Big Admin again, and if I rerun this update, we're back to 0. Exciting!
Now that we've added the products collection and we have some data to work with, let's go through the whole thing one more time, but we'll do it at a bit more of a high speed, and we'll add vendors to the mix here. So, I'll go in and I add a vendors.js file to the collections directory. Next, I'll create a brand-new Mongo collection, and notice once again, we have a global variable called vendors. And we'll talk again about that later on. Okay, so now I have a Mongo collection, and the next thing I want to do here is I want to add some seed data. So if Vendors.find.count is equal to 0, I'm going to drop in some test data, and wouldn't you know, I have it ready to go. IDs, slugs, names, and description. Good stuff. Then I'm going to loop over all of these vendors and I'm going to drop them into the database here just like I did above using Vendors.insert. (Typing) Okay, so let's do a Meteor reset, clear out the database, and then just start up our server, and everything should jam in, our products, our user, and our new vendors, and yep, there we go. Martian Armaments, Red Plant Love Machine, and Marinaris Outfitters, good stuff. Now that we have some vendor information, how are we going to use it? Well, if we scroll up here, you can see again I have IDs and slugs and so on, but each product here has a vendor with an ID of slug and a name, so a little bit de-normalized, but welcome to the world of working with a document database. That's okay. So what I need to do now is let's go over to the product page, and I'll just pick my reentry to Earth. You can see I sort of stubbed out a link here that goes nowhere, and it just says Vendor, but of course, I want to have the name of the company that makes it right here under the product name. So, let's flip back over into our template, and the one I'm looking for of course, is easy to find, because I've named everything in a particular way. Client, template, products, show, very good. Right here is the link that I need to fix, Vendor, and there is the anchor tag. But I'm going to have a bit of a problem. Now right here I can output vendor.name. That's not that big of a deal. It'll show up just fine. However, if I use pathFor and I put vendorsShow, I've got to send it some information and it starts to get ugly. So rather than do that, I'm just going to use a with block here and then it becomes easy. I can say with vendor. And I'll output the name, and then I can use pathFor straightaway. Because inside the with block, this is set to vendor, so pathFor will be able to figure out the route and the parameters that I need, and I will be needing the slug for that vendor, because in just a few seconds when I set up the route, it's going to need a slug so I know which vendor I chose. Speaking of, let's go set up routing, and yeah, if I hover over it-- that's not the right link, so whatever. Okay, heading over to router.js, and let's copy this exactly. And I'll put vendors and then slug. Good. And this should go off to vendorsShow, good, and I'll return Vendors.findOne according to the slug, here we are, simple enough. And that means I need to have a directory called vendors, and inside this, I need to have show.html. Simple enough. And then the template-- this is the important part-- the name needs to be vendorsShow. And for now, I will just do a first level header, and I'll put the name just to be sure we have everything done correctly. All right. And then let's rename our path here to vendorsShow. Good. So, refreshing and if we take a look at the link down below and the status, yep, that's exactly what I want to see, Martian Armaments, Ltd. And the URL is correct. Very nice. I'm sure it didn't surprise you that it worked, right? And let's paste in here some ready HTML. You can again get this from the downloads or from the code up in GitHub. And down below here, I'm actually going to loop out the products for each vendor, as well as the vendor information above, which you see here. That looks pretty good, but we don't have any products. Where are we going to get those products? This just means I need to have a brand-new code behind file, that's what I'm going to keep calling it, show.js, and this is going to be Template.vendorsShow again, this is the important part, that's the name you've got to remember. Products is simply going to be Products.find and this is going to go and query based on the vendors ID, which I will set to this.id. What the heck is this? Well, since I set the data in the route to be this vendor, this is going to be scoped to the data, so this.id. Down below here, then we should be able to spin out the products, and wow, Meteor's way ahead of me, it already refreshed and spun out the products, which I can now click on, and go over to the product page. That's neat. But there's still one thing I'd like to do, which is to have a link to the vendors in my navbar. So again, some ready HTML, we have a dropdown here, and it's going to drop down a list of vendors. Now how am I going to get that data? It turns out that partials can have helpers too. After all, these are just templates. The fact that I'm calling it a partial really doesn't mean anything, it simply can embed another template, we just think of them as partials. Anyway, I can have vendors here, and it can return Vendors.find, and it will return all of them. Notice that I'm not fetch, I'm using find; I'm returning a cursor out to the template, and the template knows what to do with it. Anyway, and here I can say pathFor vendorsShow, it's in a loop, so this will scoped accordingly, it will be scoped to the vendor that's in the loop. So, it'll be able to figure out the link I need, and indeed it does. Good. I'm calling this suppliers by the way, because vendors is kind of geeky, but if I click on it, it drops down, and I can view all the vendors and all their individual products.
Well, you no doubt noticed, I am working almost directly with Mongo right here, and it's really not very fun. Now you might be thinking, isn't there an ORM that you can use, or a way to have the concept of a model here? And there sort of is. I could jimmy-jack some models in here using some different things, but it's just not the Meteor way, if you will, so if you want to do something like that, there's a package for that. In fact, there's a number of them, including this one here, meteor-astronomy, and this has validations and associations and all of those things. And if you cruise down here you can see how this thing works, but it's still sort of at an early stage, but you end up orchestrating a lot of stuff with your various model. And it is an ORM, that's just the way it is. So if you like ORMs, you could check out astronomy. There's also minimongoid, and what this does is allows you to have kind of a mongoid experience if you come from a Rails background, you know what that is. But everything's written in CoffeeScript. You don't have to do this, but if you don't, things get problematic. The problem for me is I'm not a CoffeeScript fan, but if you are, and you want to have a better kind of data interaction, then this is something that you're going to want to look at, because it's quite popular and it gives you much more of an ORM experience rather than working with the bare bones Mongo kind of an implementation. There's also the collection helpers, and I really like this. It is minimalist, and what this does is basically just says, here's some methods that I want you to drop onto each model. As you can see here, a book might have an author, author might have a full name or books. You can set this in one place, and I think this is quite helpful. In fact, I know a number of people who simply refuse to work with the bare bones Mongo stuff within Meteor, and I can't blame them. Another tool you might want to check out is graviton. Again, just another ORM, and it does validations and associations and all those things. It's a little bit more wonky if you ask me, but it's also very capable. The documentation is fairly complete, but to be honest with you, I don't really care for ORMs in general. They make even less sense to me when you're working with a document database. So working directly with Mongo, it's not so bad, but I do wish I could just write my own little classes here and have some better logic tied up next to the data, but I can't, and that's okay. But anyway, I just wanted to point out that these ORMs exist, and if you're an ORM person and you like to work with them, they're here.
In this module, we're going to tackle testing and it's going to help us write our shopping cart code. Specifically we're going to set up our test environment, and then we're going to write the initial test for the shopping cart itself. Then we're going to add a cart page, and finally we're going to add notifications to the site so that when things happen, we can pop up a fun little overlay. Getting to our products page is pretty easy. We can go the supplier's route, as you can see here, and click on it, and boom we're on our product page. Or we can just go from the homepage. And that's neat and all, but what happens when we try and add something to the cart? That's really the goal of our site, we need to sell stuff. Well, it's not quite hooked up, and that's what we're going to do now using testing. So, speaking of, we have a testing tool that we can use, Velocity. It's built right into Meteor, it's pretty neat. We can install whatever framework we like to work with it, including Mocha, Jasmine, Cucumber, or the Robot Framework, so pick your favorite one. For me, that is Mocha. To add it to our site, it is, as you could imagine, just adding a package. In this case, it's mike:mocha, which I think is a great name for this package. So we're going to drop this in and then once it goes in, it's going to restart our application for us, and you can take a look at all the stuff that it added for us, including the velocity:core package and all the things that we need to hook in, as well as the chai assertion library. Now when our app starts, look at this output. Velocity is talking to us and it's saying Mocha is starting in a mirror, so it's basically mirroring our site, and that's what it's going to be testing. So if we back here to our site, up in the top corner on the right side, we now have a test runner. And it's sitting there pulsing at me. That's a little unnerving. So we don't have any tests, and if we wanted to take a look at what those look like, we can click that little button that says add sample tests, and wouldn't you know, it does. This is just crazy. I think this is one of the most amazing testing experiences I have ever seen, but there are problems. It's so easy to get taken away with a cool looking UI, but we'll see how useful it is. All right, so what just happened? It added a test directory for us, and it added a Mocha directory inside that, and then inside of here it added some test files that we can then expand on. So I'm not quite sure what most of that syntax means, if typeof MochaWeb is undefined and so on, but I do know that what it's trying to do is it's trying to separate client versus server. So, inside of here, this is our server test, so I just made it fail, and you can see the way it looks here. It's telling me, it gives me a nice output, and it's saying an AssertionError has failed. All right, that's grand. So I can come back up here and close this, and notice that it's turned red. It used to be green or blue before, but now it's red. If we want to see more information, we can come down here and take a look at the logs. We don't have any logs just yet. We can show the files under tests specifically, and there it shows our two test files that are there. That's great. And then it can actually show you the Mocha instance that is running our site. And look at that, it just shows it in a frame. This can be really important to a lot of people, because if there's a problem with Mocha running your tests, because it is running in parallel here in a mirror, you can see it right there. I haven't actually had an occasion to have to check that, but it is there, just so you know. All right, having changed that assertion back, we now have passing tests. Let's write something for real.
In the downloads for this course and also up on GitHub, I have a file called cart specs or shopping_cart_specs, I should say, that you can open up and then just drop straightaway in here. I'm doing this, I'm prewriting all these tests because, well, this course isn't about testing, it's about the Meteor testing experience, so I hope you don't mind; you don't want to watch me type tests, it's really horrible. So as we scan down through here to take a look at what I'm doing, there's a very typical way that I like to test. I want to be sure that I can get a cart, that cart has some basic information on it like a userKey, notes, items, and so on, and down below I want to be sure I can add things and remove things, typical shopping cart tests, there is nothing interesting going on here, I swear. All right, so if I save that file, it's going to tell me I have eight tests failed, but I know I've got a lot more tests than that. It gets a little bit funky here because all of the before and after hooks are failing, so it's reporting that in a weird way. Now this is what I meant; this is a beautiful test runner, but sometimes things are a little bit weird. I have a ton of tests in here, and hopefully it should have stopped when the before hook didn't go off, but it didn't. Anyway, the before hook is complaining is that I don't have a collection called carts. That's understandable; we should be able to access that. That carts collection should have a function called getCard on it, and that should take a userKey. And what this is going to do is it's going to try and find a cart using that userKey, and if it doesn't, it's going to return a brand-new cart object. Now, initially when I wrote this and recorded it, I had Carts.insert right here in the collection. This won't work from the client. It's going to cause problems. Now, why won't it work from the client? Because we removed insecure, so it won't allow this update. So, I will remove this later on, just so you know. So here in the before hook, I am using Meteor.call, that is a Meteor method. If you don't know what that is, maybe go have a review of John Sonmez's Fundamentals course, because he talks about Meteor methods. I'll touch on them lightly as we go along. First, I want to have a shoppingcart.js file. This is going to be accessible from both the client and the server, so I'm going to put it in the lib directory. Inside of here, I am going to have specific methods that happen on the server, and these are Meteor methods, getcart, addToCart, removeFromCart. I don't want people playing with our carts directly. I'd like to make this all explicit using Meteor methods, and this is how you do it. First thing I want to do is go back here and turn these comments into code, and I do that by declaring functions here, and this one's going to be getCart, this is the one I'm particularly interested in right now, and this is simply going to return Carts.getCart. And then addToCart, pretty much the same deal. It's going to take a userKey for the cart and it's going to take a sku. And then finally removeFromCart, you get the idea. It's going to take a userKey, a sku, and there you are. The last thing I need to do is I want to have a saveCart function that I can use, because in the saveCart function I'm going to do some de-normalization if you will. I want to make sure that the cart total and the cart count are correctly added up, and that's what I'm going to do, I'm just going to loop over all the items, and there you are. And finally, I'll just do a Carts.update. I'll pass in our filter, and then finally, I am going to make sure that upsert is set to true. That way it will insert the cart if it doesn't exist. Next, I'll just fast forward a little bit, because writing code is fun, but sometimes watching other people write code is not so fun. Anyway, add to cart, here again, this is all standard stuff. If the item exists in the cart, I'm going to increment the quantity, if it doesn't, I'll push to the array, and then I'm going to use Meteor.call to save the cart. All of this code is on GitHub or part of downloads, and you can take a look at it there. All right, let's jump back over to Velocity. And it looks like I've got an error. It's reporting that key is not defined, so let's go fix that really quickly, and that would be in our carts.js file over here, and yes, there we go. There we go, we have a small error there. And hopefully everything should pass. Oh, one more, two more, hmm. Object object Object has no method bySku. I know what that is. That's in our products collection here. I had a helper method in there that returned products by sku. It looks like I accidentally deleted it. So, now that is fixed. That was in our addToCart method, it pulled a product out of the database, and would jam it in. And let's see, now I am up to three. Wait a minute, these things should be going backwards, what's going on here? Hmm, it looks like I have a pretty funky error, expected 36996, what? None of that makes sense. If you find things that just don't make sense, try restarting your server. This is a weird thing, and look at that, just restarting the server made everything work. So, I don't know what the cause of what you just saw was, but I maybe was just going too fast, because as I was writing tests and saving files, things were restarting all over the place, and I might have been going too fast and, who knows. Anyway, just to point out that if you do find problems with the runner as you saw right there, just restart your server. Next thing I need to do here is to be able to call those Meteor methods from the client. So I'm going to use all the same names. Here I'll just say getCart, and then I'm going to return Meteor.call getCart. So I don't have access to the Meteor methods here directly from the client, I have to make sure I write an asynchronous method for that. Okay, so for this, there we go, just to make sure I can reach it on the client, good, and I can, excellent, and it's returning a cart to me. That's exciting. Now I just need to go back and fill out the client side of these Meteor methods. And I'm again going to use all the same exact method names, getCart, addToCart, removeFromCart, saveCart, so on, and once again I have a bit of a bug here, calling saveCart basically defeats the entire purpose of separating things. Oh my gosh, if a user just wants to alter their cart, they can. Set everything inside of the cart to 0 and then I get to send it up. So, yes, Meteor methods provide good clean separation, but it's pretty easy to do something dumb as you saw me do right there. All right, let's wire this thing up. Here I have a button here called add-to-cart, and what I need to do for the products page is simply wire up some events, and for that we use some backbone style syntax to wire the event, the click on the add-to-cart, we want to prevent the default, because that would try and navigate the page, and then what we want to do is just call addToCart, passing along the sku, which I can do with this.sku, because the data is coming down to us from our route, and then here I'll just console.log the error and also console.log the result. I want to be sure that things are happening the way I expect. And clicking on Add To Cart, hurray! I get a cart back, and it's got an item in it. Isn't that exciting? That's exactly what I wanted to see.
The Cart Page
Well, obviously alert boxes are a horrible idea. I'd rather have a really pretty overlay notification. If you go to atmosphere.js, of course, there is a package or 12 for that. The one that I'll be using for this is juliancwirko, the s-alert package. It's really neat. If you go and look at the demo, it's amazing as you're about to see. The next thing I need to do is to configure this thing, and I've created a file called notifications.js, and I've dropped it into the config directory for the client. Here is the default configuration for sAlert. If you don't need to change anything, you don't need this file, but I do want to change things. I'm going to reset the position to be the bottom. I'll leave the time out at five seconds, and stack I'll leave at true, because I want to stack the alerts. Next I need to give sAlert a place to output. So I've created a notifications.html template, and I'll let Meteor inject this into the body as you see here. This is just a standard way of doing things with sAlert. All right, that's pretty much it. The next thing I need to do is to have some triggers for when I want the alert to go off. So here on my cart page, I'll do a quick check on the number. Let's just say if it's not a number, then I want to check it. In addition, if I have any errors down below on save, and if I have success as well, I want to notify the user. To do that, you just say sAlert.error, or sAlert.success, as you see here, and then you just pop the message. It's as simple as that. Not all that much configuration to have this. I think this is beautiful. And it took me all of a minute or two to configure and set up. I think that's pretty neat. Again, one of the major strengths of Meteor is the community and the package system. In this module, we set up testing with Mocha for our shopping cart. Then we added tests and made them all pass. We then rounded out the cart experience adding a remove and update quantity feature. And finally, we added notifications for a little bit better user experience. Now if you're wondering, wait, wasn't this module supposed to be about testing? And yes, it is. However, it is so easy to test with Meteor, I pretty much showed you what I needed to show you in about 10 minutes. And as I keep saying, that is one of the best thing about Meteor. If you want to do something, like in our case, testing, there's always a package out there for you. We wanted to use Mocha, so we dropped it in and wrote our test. Since you're an experienced developer and you've no doubt used Mocha before, I don't really need to show you how to use a framework or how to test in general; you already know how to do those things. I just needed to show you how to plug it into Meteor, and that's what we did.
Installing the Tools
Routes and Templates
We've been through this drill before. We need to add templates and a route so our checkout page shows up. Let's do that now. Repetition is a good thing when learning a new system. So, first thing I need to do is to create a directory called Checkout, that's where I'm going to keep my templates. I'm going to have my template name reflect these locations on disk, so it's going to be checkoutShow. There we go. And inside of here, I'm just going to simply put a first level header to make sure that the page shows up as we expect, and I'll just say CHECKOUT, hooray! All right, now that we've done that, let's go to our router and we'll add a route for checking out. We don't need a parameter here, because a checkout page is going to use the current cart, so we're all right just leaving it exactly like this. Okay, we'll set the name to checkoutShow, and we are ready to hook it up. So, let's head over to our cart page, and what I can do here is I can set the click event of the checkout button and I can say Router.go, but it's so easy to get stuck in that mindset, you don't need to do that. All we need to do is just set a link. There's no need to trap an event; let's make things easy on ourselves. So, we'll do pathFor 'checkoutShow' as you see here, and that'll take us to the checkout page, ideally. So, let's go check out and there it is. Checkout. Very good. Creating a template, setting a route, linking to that route, we've got that down. We've done it enough times so far. So let's drop in the HTML we need for our checkout page. You can find this in the course downloads, or you can grab it from the GitHub project up on GitHub. Here you can see it is a typical checkout page, sort of. I'm doing something a little bit interesting in here, which is that I'm working with Knockout, and the reason why is the templating system for Meteor is pretty good, but I want to go beyond that. I want to do some interesting whiz-bang stuff, and I'm going to do that using Knockout and all the data bind syntax. I haven't hooked it up yet, so our page is kind of broken, but hey, look at that, at least it's showing up. That's a start. In the next clip I will hook up Knockout and the view model.
If you've never used Knockout before, this might be a little bit strange to you, but I'll try and explain it as we go along. I need to create a view model that Knockout is going to work with, and to do that I'll drop in the checkout view model right into the checkout directory to keep it all together. And to remind you, I have Knockout and Stripe checkout in the scripts directory here, so they're already part of the project, and I can just use them directly right here. I'll create a variable called CheckoutViewModel, and this is standard Knockout stuff. I have some properties set above as observables, name, email, and description, which is computed, and then I have a shipping address down below, and I'm looping over the keys of any arguments passed in, looking for an address. And if it's there, I will assign the shipping address. In fact, I'm using the arguments for the name and the email as well. Down below I have a set of Boolean flags that I'll be using to update and change the way the page looks. Specifically I need to know if the user has accepted the terms. And then I have two methods here, showCheckoutForm and showProgressBar, which you can see. These are going to set in the background, because what I'm going to do is I'm going to switch out whether I'm showing the form or that nifty little please wait a minute while we redirect. Down below that, I have some simple validations that are going to tell the user what they need to fill in and so on, I'll talk more about that in a bit. Next up here is the loadStripeCheckout event. This is one of the more important things. This is going to interact with the StripeCheckout library. First thing I need to do is to configure Stripe itself; that is our handler. I am setting a key that I need to send along and that is going to be set in Meteor settings. We'll talk more about that in a little bit. Next I have an image that I want to set for a little branding at the top of the little checkout box. I want the zip code from the user, and I want to preset the email using the view model's email field. Then, allowRememberMe I'll set to true, and I will set a flag here that I want to have the user have to put in an address. Finally, I will tell it that when the token comes in it needs to fire the handle token function, which I have defined down below. Finally, to finish off the click event, I'll just call handler.open, and it opens up the Stripe checkout box with the name and the description and the amount. After the user has put in all the information and hits submit, it's going to bounce out to Stripe. Stripe's going to cycle it and send back a token if everything is successful; otherwise, the user will get an error. When that token comes back, this handleToken function will be fired. I'm going to scroll back to the top of the window, and then I'm going to turn off the checkout form, showCheckoutForm false. That's going to show the redirecting status bar. Then, I'm going to construct a checkout object. This is the data that I am going to send to the server. I want this to be in a very specific format that you see here. I want name, email, description, IP, I want to send in the shipping address, as well as the billing address. In addition to these things, I'm also going to send along the full token, as you can see here. Finally, when all that's done, I'm going to call processPayment. This is going to be a Meteor method, it's going to hand that checkout off to the server, and then we're going to evaluate the result. If everything is good, we will redirect off to a receipt page. Okay, checking out our page, you can see that everything is broken. I don't have Knockout hooked up yet, it's not taking in that view model, so none of the data binding stuff is even happening. That's okay. I just want to make sure that I don't have any script errors, and I don't. Here in blue, you can see the prompts that I want to show the user interactively as they fill out the form. I don't like red form validations. I kind of find it to be a little bit offensive. But you're filling something out, and you think you've done it right, and the form's like no, you suck, you didn't do it right, go back and fix it. Anyway, so I'm going to use Knockout to control the flow of that. Okay, onward. So, what I need to do now is to have a bit of code behind for our checkout page, and for that I will create a show.js file. And then inside of here, I will create some helpers for our checkoutShow template. Specifically I need to have the cart, and in this case, I will need to use a global variable, because I'll need to use the cart information inside of Knockout. So I'll do the same thing that I did before with the cart page. The next thing I need to do here is to instantiate that lovely CheckoutViewModel that I just created, and I'll do that in the onRendered event of our template. This is a way to make sure that the template is in the DOM before you have anything else happen. So inside here, I'm going to new up our CheckoutViewModel and I'll send in some arguments in the form of test data. This has a test name and an email and address and so on. Next I need to grab the DOM element that contains all of the Knockout bindable bits, and the reason why I need to do that is because since we're working with Meteor, there is no page refresh, so if we do a Knockout binding, it's going to be remembered, and if we come back to this page, all those bindings can get fired again and we're going to have an error. So, to get around that, I am going to grab that DOM element and then I'm going to use ko.cleanNode, that's going to clean off all of the bindings that are already on that DOM element. Finally, I'm going to reapply the bindings. Now, I don't know of a better way of doing this. I've looked all over the place and how to deal with the error, and people would say, oh, there's a better way of dealing with it. I never found out. People would say there could be a performance problem. Well, frankly I haven't seen it. So, this is the best solution I came up with. If you know of a better one, please do let me know in the comments. All right, having done all of that, let's go back over to the browser and see if our load up is working and that Knockout is actually firing. Go over here, yep, that's looks pretty good. So all the bindings are going off, the rendering has happened as we expect. Here you can see their little prompt. But if I take away the shipping address and go down below, you could see the prompt grows. We still need your shipping address. If I go up here and remove name and email and go back down below, we still need your name and email. So the prompts are firing and loading as we would expect. If we put this information back, actually I'll just refresh the page and go back down, you can see that all we need is for you to accept the terms and conditions, and once you do you get a green box. Good to go.
Previously in the last clip when I was going through the checkout view model, I mentioned that I would talk about Meteor settings, because that is how I'm setting the Stripe key. This is very important. You want to be using Meteor settings for this kind of thing. You do not want to hard code this or come up with your own way of doing it. So down below here I have two files, settings.development.json and settings.json. And if we take a look at settings.development.json, you can see we have a few keys in here, stripeSecretKey, which I am going to erase and redo, and our stripePublickKey, and what these are are just keys if we're working with test data up on Stripe. In settings.json, this is going to be for production use. So the development is for development and this is for production. Anything that you label public is viewable by the client. If you don't have it under public, it's only viewable on the server, and that's really important to understand. In my case, I need that public key on the client, so I set it under public. All right, why are we doing this? The primary thing is you want to have these settings file ignored. You do not want sensitive information stored anywhere ever, even if you have a private GitHub account, or you have a private repo of your very own, do yourself a favor and do not check things in, they can be discoverable and when you least expect it. Okay, off my soapbox now. How do we actually use these settings? You have to tell Meteor to load them. I think this is a little silly. I think if there's a settings.json file, it should be loaded, but it's not. You have to specify where the settings are, and I guess that makes sense. So, here I'm going to tell it to use the settings.development.json. When I go into production, I'm going to tell it to use settings.json. Actually Meteor up is going to do that for me. We'll talk about that in the next module. Okay, now that I've done that, my settings have been loaded, so I can use those settings now back on the checkout page. And again, when it comes to production, we'll load those settings from our settings.json file, and those will be used in place of our development settings.
I've created a Postgres database here, and a table called checkouts. And you can see the SQL for it right here. You can grab the SQL in the project downloads, or again you can get it from the project up on GitHub. Here I'm just creating a checkouts table that almost exactly mirrors the structure of our view model. I have an ID, I have a card ID, email, name, description, items using jsonb, same with the billing address and shipping address using jsonb. I'm going to save the token, once again as jsonb, and the payment details, which is going to be the Stripe response, once again, jsonb. I love Postgres. So we're going to be using NPM to save a query to the checkouts table directly. However, it's worth mentioning that you really should be using a message queue for this kind of operation. Ideally make it asynchronous to the background, send an email when the order is ready, or make it synchronous if you want, but have some fault tolerance. Message queuing is a good idea, I just need to say that right now. Anyway, so what we're going to be using to make this all happen is the NPM package from meteorhacks. Using this thing is kind of interesting. Installation is simple, and getting your packages in is also simple. All you have to do is add meteorhacks:npm as you see here, and then to define your packages, you simply need to fill out a package.json. And this is not to be confused with the node modules package.json, this is just a file that's a JSON file. However, when you want to call and use anything from NPM, you have to use Meteor.npmRequire and then you have to wrap it with this Async.runSync, because as you've noticed, everything in Meteor likes to pretend its synchronous, and in this case, NPM stuff usually has callbacks like ours do, so you need to wrap it with that Async.runSync. Now it looks a little bit hacky, but once you do it once or twice it's not all that bad, and we're only using this in one little spot. Okay, NPM has been added and it's telling us down below here that a packages.json file has been added to our project. We can go in there now and we can specify the node modules that we want loaded. And then, of course, we need to reload our project. Sounds good, fairly straightforward. If we go over here, there is a packages.json, which is an empty JSON page. And what I want to do in here is specify that I want to load up Massive, and you have to have a version in here. If you don't know the version of the package you want to load, just go and read its package.json up on GitHub, or at NPM.js.org. So for Massive, this is going to be version 2.0.4, for Stripe it's going to be 3.3.4. You can also put a GitHub address in there with a specified tag if you want. All right, so let's restart our application, making sure to send in our settings, and we should get a message, there it is, npm-container, it is now grabbing Massive and Stripe and it's going to drop those NPM modules for our use later on. And there it is, it didn't take too long, and we are running. No errors, that's excellent.
Stripe and Postgres
Now that we have access to our Massive and Stripe packages, I want to set up a little bit of abstraction so I can work with them in a same way. I'll set up a little repository for our sales_repo. This is just going to be Postgres for now, that one simple table. And on Meteor.startup, what I'm going to do is I'm going to declare once again a global object that I can use throughout our project here. The next step is to require Massive and to load it up. When Massive loads up, it actually looks at your database, reads in all the tables, and attaches them to an instance that you can use later on. I should also mention that there is a loadSync method that doesn't take a callback. I could use that here, but using the callback works just fine. So we start out by requiring Massive, and then I have to load it up, so I have to say Massive.connect. That is going to give me an instance. Then I'm going to set a db variable, as you see here, to the result. That result is going to be the result of the callback. Then, down below I have some additional methods, save, checkout, and getReceipt. Once again, this is going to use Async.runSync, and then I'm just basically wrapping up checkouts.save. Checkouts is our checkouts table, I could just call save and pass in an object and it will do the rest for me, which is nice. And then I just need to call done. Once I call done, that's going to pass back to the save result there, and that's going to pass back the results of our query. For those of you not familiar with Massive or if any of this looks weird to you, this is just basic database interaction. I'm not doing anything tricky here. The only thing that's particularly tricky is I'm wrapping all of these functions with Async.runSync, and when I pass done back in, that is going to return out a result in a synchronous way. That's really all you need to know right now. That and I also have a global variable called SalesRepo, and I can SalesRepo.saveCheckout, which I will do in just a little bit. All right, the next thing I need to do here is to create a little wrapper for our Stripe service, and I will call this stripe_service.js, again, putting it in the server folder. I don't need to access any of this on the client, so I want to make sure it's locked away on the server. Inside of here, I'm going to do the same thing. In Meteor.startup, I'm going to create a StripeWrapper prototype. This is typical Stripe API call stuff, once again, wrapped in Async.runSync. You'll also notice here that I'm using Meteor.settings to grab the stripeSecretKey. These are not public, but they are viewable on the server, not viewable on the client. That's important to remember. Down below here is your typical stripe.charges.create. I'm just passing in a charge object using the token that was passed into us with the checkout object, and then I'm going to return the results of the call. And a little extra credit for you, if you want to, we need a refund charge method. You can go ahead and add that if you like, and make sure you email me, and I'll make sure you get the extra credit. All right, this looks fairly complete. One thing I have to say one more time when doing things like this, saving records, running charges in sequence, you really want to have some kind of message queuing system. If you're familiar with RabbitMQ, you can plug this in instead of plugging in Stripe and Postgres as I've done here, I'll leave to you, and also for an additional course later on. All right, we are ready to go. I have my Stripes service public variable down below, I've got my SalesRepo ready to go. I could now start saving and running checkouts. That's exciting, we're getting to the heart of the matter. That is coming up in the very next clip.
Well, we left off on the client last time by saying that we're going to call process payment after the Stripe checkout bits come back. I still have yet to make that function, so let's do that now. I'll create a brand-new file here and call it payment.js and stick it in the lib directory. The reason is I need this accessible by both the server and the client. However, I only need parts accessible from the client, and I do that by saying if this is the client, then allow these methods, and if it's the server, then I want to declare these methods. This is a nice way of keeping logic together while separating client and server. We did this already for the cart page. Okay, so what I'm going to do is create a few methods here called processPayment and getReceipt. We'll talk about getReceipt more later on. processPayment is what I need to call when the token comes back from Stripe. That's going to call a Meteor method by the same name, process payment, which is down below. This method is pretty straightforward. We're just going to grab the cart out of the database, and we're not going to accept it coming in from the client. That's not a good idea for all the reasons that we said before. So we're going to go grab the cart out of the database, then we're going to set the total on our checkout from the cart itself. We're going to pass along the items, and this is important, you have to stringify all the cart items; otherwise, the jsonb stuff in Postgres is going to strip it off. It's just a weird thing. The jsonb data type does not like an array. As a root level object, it wants to see a label there, that's what it considers proper JSON, so we're going to have to stringify it, which is okay, it goes in still as jsonb. Next were are going to set the payment details using our StripeService.runCharge. That is going to bring back the entire response from Stripe that is called right here from our Stripe service, as you can see, stripe.charges.create. That's going to be a whole mess of data that we can access and use later on, and I want to make sure I trap all of it. So that's where that comes from. Then we're going to set a reference key using a handy Meteor helper method called Meteor.uuid. This is just going to be a GUID that we'll use later on as the receipt ID. We're going to empty the cart and then we're going to save it into the database. If things don't go right with our saveCheckout routine, you'll see the response we'll get back. We'll get back this object that says success is false, and it'll pass back the error. If everything goes well, we'll get a receipt ID, which is going to be the reference key, and then a little message that says thank you, and success being true. That response object is going to make it all the way down to the client where we can interrogate it. If we have success, we are going to just redirect to receiptShow. This is a route that we haven't made up just yet. And then we're going to pass along the parameter ID. However, I want to see the results straightaway in the console just to make sure I know what's going on, so I'll just console.log the result right here. If we have a problem, then we're going to pop up an S alert as you saw down below. Okay, I think we are ready to take this thing for a spin. Let's go back to our checkout page, and it looks like we have all the data that we need. The only thing left is to accept the terms and conditions, there we go, and I hit pay now, and this should launch the Stripe checkout box, and it does. Branded nicely with our name and our logo, that's cool. I wish I had a little more real estate here to show you this box, it is so nice to see, but I have small screen. So notice up here on the top right side, it says we're in test mode, that's what we want to see. And what I'll do is I'll just prefill some of this stuff in Joe Blow from my Chrome form filling stuff. And here let's put in a test card of 4 and all 1s. Stripe has a bunch of test codes that you can use if you want to see what happens when a charge is declined, and so on. Down below the total is on the button. Clicking it, and yeah. I love this, this is all Stripe's doing. This isn't any of my code. I just told Stripe what to do. We get back a success object, that is great. That's telling us that our data went into the database, it's giving us a receipt ID. Let's go and verify that. Let's go and take a look at our checkouts table, dropping this thing open, and there it is, there's our checkout, the reference key matches. And taking a look, and let's just verify that that GUID is the same GUID here; it is. Good, so that is going in. So let's go check out all the data that we have. We have a cart_id, good, we have created_at, good, email, name, IP address, we pulled that off of the Stripe information that came back, this is great. All this information, the only thing that we have from the user is their shipping address, name, and email. The rest of it is information gathered by Stripe, which I believe we can trust. Yeah, this is great. All right, so it looks like we have all the information that we need here, our checkout is going in, the only thing left for us now is to get a receipt page going so the user can see their information about their order.
Our checkout information is being stored in the database. We are getting a redirect prompt, which is great. Now we just need to create a receipt page so the user can see their order. So, down to business, it's the usual thing here. Create a directory called receipt, and in it we're going to create two things. First is show.html, which is going to be a template, and the next is I'm going to need a code behind file, so I'll create show.js. And for our template, we have some ready HTML once again in the downloads of the project, also up on GitHub, go and grab it, and drop it in. And here you can see that I'm using a with block, and this is important, why am I doing this? Well, the main reason is that if we scroll down to the very, very bottom, you can see I'm using each, which is a really neat thing about the Handlerbars inspired templating language that we're using with Meteor. Else gets fired if we don't have a receipt object. So here it's just telling me that there's no receipt with that particular ID, and then it's offering me a link to go buy something else. So that's good, that is why I'm using this with block. Okay, we have our template, looking pretty good. Now let's go over and create a route for this receipt, and I need to be able to accommodate people who maybe don't have a membership account with our site. So I'll want to be able to pass in the GUID token ID, so anybody can view their receipt without having to login or have an account. All right, so let's give this the name receiptShow following our typical pattern. And then here, I'm going to pass in some data. But here we actually run into a bit of a problem. You wouldn't think it would be a problem, so let's dissect this a little bit in what we're trying to do. I have a getReceipt method here on my SalesRepo, and this just takes a reference key, which is that GUID. That is going to run our Async.runSync, so in other words, it's going to make our database call synchronous. All right, so then I'm going to say checkouts.findOne, which basically says go find the checkout with this reference key, and in fact, I'm going to console.log the result right there in line just to prove to you that it is working, but something weird is happening. So here I need to use Meteor.call getReceipt, because the router is on the client, and so I need to be sure that I am calling this server method using Meteor.call. And I'm passing in this .params.id. And that should work. Now all you should have to do is say return and then set a key for receipt, and then set it to a value, the receipt itself. That should work, shouldn't it? It's a synchronous call, I should be able to have the data. Well, let's check it out and see what happens. I'll head over to Postgres and open up our checkouts table and grab that reference key that we just jammed in there. And then I'll head over to the browser. Now that we have our route, I should be able to put in receipt and that ID. And look at that. There is no receipt with that ID. That's interesting. As I said, something weird is happening. Let's go take a look at our terminal. And the terminal has output returning now 8. That's that console.log statement that I put in our SalesRepo, so it is returning, but why isn't it showing up on the page? Let's see if we can find out. I'm going to console.log the receipt right here in the route. This is going to rebuild the application, and we're going to go back, and so the page already requested the receipt, because it reloaded itself, and so you can see returning now 8. Let's reload it one more time and go back. Yep, look at there, console.log says undefined. We did the console.log in the browser, that's why we saw it down there in the console there. The receipt is undefined in the route, but here it's clearly not on the server. All right, I'm laboring this a little bit I guess, but basically the deal is this. The data that is expected in this route here is reactive data. If you're using Mongo and you're using a cursor, that is reactive data. A simple call like this is not reactive data, it's just data, and Meteor does not want to wait for it to come back. So it's just going to render the page. The idea with Meteor is that the page will render and then the data will show up when it shows up. In this case, we don't have a facility for that. I'll tell you right now there's a lot of ways that you could try to get around it. There's a package called reactive var where you can set a reactive variable. I tried to set that as the data, eh, it didn't work. I tried a whole lot of things. I spent a good six hours on this one problem right here, pulling my hair out; I wish I had a better answer for you, but basically the deal is this. I am going to return the ID, that's all I really want as data on the page, because I'm going to handle this all on the page itself in the code behind of our receipt page. So the first thing I'm going to do is I'm going to create some helpers for our receipt page. And in here I am going to do what you've seen me do before. I'm going to create a function called receipt, and that function is simply going to take some data and go and request the receipt from a Meteor method. You might say, how you doing that? Well here in our payments.js file, I do have a client method called getReceipt. That is going to do a Meteor.call getReceipt, and it's going to have a callback, which is a problem. How am I going to work this with a callback? This right here is again expecting to work with reactive data. Oh, boy. What am I going to do? Well, I'll tell you what I'm going to do. I'm going to do something a little bit hacky. I'm going to create a variable called receiptData, and this is going to use Blaze.ReactiveVar. This is a reactive variable. It means if the data changes in this variable, it will ripple out that change to the screen. Blaze is the templating engine, in case you're wondering. So, instead of doing a getReceipt, which takes a callback, and I can't use callbacks in here, instead of doing that, I'm simply going to return the receiptData variable. That is a reactive variable, our template can use that just fine. We can change it as we want to. So what I'll do is use the onRendered function here. What this is going to allow me to do is it's going to allow me to grab the receipt ID that was passed down to us from the route. And then I can call getReceipt. So, I'll use this .data.id and in here, callbacks are fine. This'll just get fired whenever the template renders, and whenever it's done I will set receiptData using receiptData.set, and I'll pass in the result. I know a little bit secure it is, but I find this pattern quite useful. We're able to use reactiveData, we're also able to use methods that take a callback. All right, so let's try it. And wouldn't you know it, it doesn't work. Oh, boy, these kinds of things just happen to you. You know, we're getting so close to the end, and wouldn't you know, it's the very simplest thing that we spend the most time on. I spent so much time on this, and I thought, hey, we're almost done, this is easy. All right, what's the problem here? Let's console.log this .data.id. What's going on? Something is not being set properly. I'll refresh this page, taking a look at the console down below. Undefined. That's weird. It seems like everything I'm trying to set on this route is undefined, what is going on? Okay, deep breath. I'm looking for this.data.id and it seems to be undefined. Let's go check the route and see what the data looks like that I am sending down, and I'm-- ick-- sending a naked GUID down, that's the problem, I'm not setting an object with the ID field set, oh geez. Rob! Okay, refreshing. I should have some data, and I do. There is our lovely formatted receipt. Success! I love it! Finally, a nice checkout process with a receipt page end to end. I think we are functionally finished here. That makes me happy. The only thing we have left to do is to go into production and show off our good work. And we'll do that in the very next module. In this module, we installed all the things we would need for our checkout process, including Stripe and Postgres. We used Knockout to drive a friendlier checkout page. I could've used the Meteor templating stuff, but I think Knockout did us right. We used NPM to work with Stripe and Postgres, wrapping up a repo and a Stripe service object that I think are pretty functional. And finally, we went into the weeds a little bit with our receipt page, and discovered some interesting things about reactive variables, reactive data, and our NPM stuff. I think we did a good job, and I think our checkout is complete.
Our site is feature complete and ready to go, good job! Nice work. Now we need to push it into production. Before we do that, we need to fine tune and tweak things, and then dive into publish and subscribe to make sure that the data that we're sending around is okay to send around. Then, we're going to get our site and server ready with MeteorUp. Finally, we're going to take a look at hosting services to make deployment easy. You've probably noticed as we've been coding along here that we keep getting this weird message from Meteor that we have set up some data subscriptions with Meteor .publish, blah, blah, blah, turn off autopublish. It keeps prompting us to do that, and the reason is, which I mentioned before, that autopublish and insecure are enabled by default, and that is not a good idea if you want to go to production, and we obviously do not want that. So if we do a Meteor list and take a look at all the packages that we have installed and enabled, you can see that we have autopublish in there and that is publishes the entire database to all clients. We clearly do not want that, because our database, well, some of the data we probably want to hold back. If you're familiar with Meteor, you know what I mean already, but if you're not, let's just take a quick detour here. If I say show me all the products, Meteor will. It's not that big of a deal. But do I need those products front-loaded for everything? What if I had 1000 products in the database? Well, you could see the problem here. But more importantly, what if I do Carts.find().fetch()? Here I can grab all the carts in the database, and that is not good. That's a bit of a security issue more than a data load issue, so we have a couple things we need to figure out here, namely that we need to stop publishing all the data straightaway. I could take a look at everyone's cart and see what they're going to buy. That's not so good. So let's get rid of this thing. How do we do that? We just say meteor remove autopublish. And this is going to be quite easy, it's just going to take it right out, and boom, it's done. However, we have a problem immediately. If you've used Meteor before, this is going to be no surprise to you. If we say Carts.find().fetch() now, no carts. But what about our products? If we do Products.find().fetch(), we have none. That's because we're not publishing any data at all from Meteor, and if we come down here, not even the featured products are making it through. To understand what's going on, let's take a quick detour into the world of publish subscribe. Let's take a quick detour here and discuss a fundamental aspect of Meteor, publications and subscriptions. So far I've only discussed this topic in passing, but we're at the point now where we need to know it in depth if we're to secure and tighten down our application. The main thing to understand is that Meteor wants to be real time, meaning that changes in the data in your application are immediately reflected to your clients. You've probably seen the demos where you update the data and the client, and it's immediately reflected on the page. This isn't all that special. Ember, Knockout, and other frameworks do two-way binding as well, but Meteor goes beyond that. If I update the record in the database, you can see the change is immediately pushed down to the client, which is great. But how is this achieved? The short answer is through publications and subscriptions. In Meteor when you set up a publication for your data, you're making it available to any client that wants to subscribe to it. So far, we've had autopublish turned on, which means that all the data in our database was available for a subscription. In addition, autopublish set up subscriptions for this data for all clients. This means that any changes we make are automatically rippled out to all clients. This makes good sense when you're building your application, and it keeps you focused on setting up functionality, as we've been doing here. But now that we have a working prototype, we're going to want to be sure to lock things down a little bit. As you can see from this screen shot, we have two problems. The first is that all the product data is sent down the wire to our client in a payload. That's overkill and can be a massive scaling problem. The second is that all of our carts are visible too. That's a bit security hole, as well as a scaling problem. We'll need to limit these things to make our application more efficient and more secure, as well as more responsive. If you've ever worked with a single page application, you know that they can tend to be just a little bit chatty, calling back to the server constantly for data loads, saves, and updates. If your server is under load from a ridiculous amount of Ajax requests, which can easily happen, just remember the healthcare.gov debacle in the United States when Obamacare went live? Oh, man. Your user experience can nose-dive quickly-- your server can actually shut down. With a real time application like ours, would our app be running Ajax requests constantly? Would this crater our server? The short answer is no. It's also yes, but really it's no. It's kind of both. Meteor has some tricks up its sleeve to mitigate chatty applications, and also to make sure your users don't see a slowdown. This is where you get to know a lofty little term called latency compensation. What this means is that Meteor will make up for the fact that your server might be a long way away and waiting for data to come and go. Latency is a bummer. So Meteor compensates for it with latency compensation. We talked about this a bit in the first module getting your head around Meteor, but it's worth bringing up again now that we're about to get into production and we're trying to tweak our publications and subscriptions. Whenever a subscription is created and used, the data is mirrored on both the client and the server. If that data changes on the client, the affect is immediate on the client. In the background, Meteor synchronizes the data on the server. If there's a problem, the data from the server will win out and the client change will be reverted. We've already seen this with our products collection when we implemented allow and deny. If there's not a problem, everything proceeds normally. This gives your users a sense of speed, but it's important to understand that changes in data may appear immediate, but can take awhile to actually synchronize. On slow networks this can be problematic, especially if your users make changes and instantly close the browser; that's not good. So we've already taken a great first step by turning off autopublish, but now we have no data at all to work with, which is okay, because it means we now need to think about what data each page needs, and then to prepare a specific publication and subscription setup for that data. That's what we're going to do now.
Publications and Routes
Autopublish is turned off and we have no data, so let's fix that, and the way we do that is by setting up a series of publications that each page is going to need. So we'll do that on the server. That's important. We don't want to set up publications on the client. So we'll just have a single file here called publications.js. And in here, I will simply add a quick method for publishing the featured products that we need to see on the homepage. And I'm going to call this publication featured-products. No need to be tricky here. This is simply a function, and what this function needs to return is the data for this publication. So here, I'll just reuse the Products.featured method. And there it is. That's what a publication looks like in Meteor. It's that simple. But then how do we actually subscribe to that data? We can set up in our route here. There is a subscriptions key that we can use, or we can tell it to waitOn, which is what I like to use, because what waitOn will do is it will wait until the data is actually ready and then the template will render. So here we're just going to say waitOn, and then we're going to see Meteor.subscribe, and then we're going to use the exact name of our publication, and then pop that in right there. And that's all we need to do. This is a very typical publication and subscription pattern that you see a lot in Meteor with publications being set, and then waitOn being used to return in Meteor.subscribe. All right, well how is the data actually getting pulled? That's right here. Don't confuse Meteor.subscribe with the actual data query. That's what I did when I first started getting used to Meteor. Now, the query is still happening here in our homeIndex helper functions, which is as it should be. All right, so let's go back to our homepage, and boom, there is our data back. That's a good thing, because that's the only three products that we need on this page, we don't need to the entire product drop. But we don't have any vendors. All right, well, that's easy to fix. Let's go back into our routes. We're going to need vendors everywhere. So we're going to want to plant that subscription right here in the configuration. So we can say that we want to globally wait on all of our vendors. So here we're just going to say return Meteor.subcribe to vendors. Now if we go back and refresh the page, we're going to see something interesting. That is our loading functionality, and it is waiting on a subscription, and we have a problem. That subscription doesn't exist, so that loading page is going to sit there forever until it bonks out, so we need to explicitly declare Meteor.publish vendors, and it's going to be all vendors, and then we'll return Vendors.find. Now, it's important in these subscriptions that you use .find, because that returns a cursor, that's what the waitOn expects. There we go, there's our vendors. This page looks all right. We have vendor information, but we don't have any products. Can you guess why? We don't have a subscription for those products yet, so we're going to have to set that up as well. Heading back over to publications, we want to publish all the products for a given vendor. To do this, we are actually going to take in a parameter here, and for that we just simply create a function, and then specify the parameter that we expect to see. In this case, it's going to be the slug of the vendor, that'll be Martian-Armaments, or whatever that you see in the URL, as you can see, martian-armaments. We're going to send that in and we're going to use in a query to return the vendor. So here I just need to say return Products and use a little MongoDB action, saying find, and then vendor.slug is equal to slug. Make sure you put quotes around vendor.slug; if you don't, Mongo is not going to have any idea what you're talking about. Okay, and then here we're just going to put a waitOn specification for that subscription, waitOn, and then Meteor.subscribe, and then products-by-vendor. Heading back over to our page to see if it worked, let's refresh, and yep, there they are. That's great. So you can set up subscriptions based on certain parameters that you need to pass in. So if I change vendors over here, you can see that we now have access to these three products and this one vendor. If we go down to the console just to check this out, look what happens if I put Products.find().fetch(). I only have three products available to me. That's because the only subscription I'm operating on right now is the products by vendor. This might seem a little weird when you're getting used to it, but it's actually fundamental to Meteor and to understanding how publications and subscriptions work. Tightening the UI down as we're doing right here is really good for performance and for security. Okay, let's head over to the product page and, it should come as no surprise, we don't have any product information. We need to set up another publication and subscription for this. So can you guess what we're going to do? Let's set up another publication here, and we'll call it Meteor.publish-- actually, I'll just copy and paste this, that's a little bit easier. And then I'll just say products-by-sku. Take in a sku and then we'll say Products.findOne. I think that'll work. We'll see. And then we'll specify the sku that we're looking for. Now that we have a publication in place, let's go over and set up the subscription, and so I'll go over to our router, and here in products and sku in the productsShow route, I'm going to say waitOn, create a function, and then I'm going to return our Meteor.subscribe, and then products-by-sku. Heading back over to our page and refreshing, we still get something interesting here. The page is just spinning and then nothing. The reason is findOne won't work with a publication. This is something you're just going to have to memorize. Publish expects a cursor, and that is because publish wants reactive data. findOne just returns a single record, and find returns an actual cursor that can be reactive, and there you go. You can see our data. We even saw our loading screen fade away as the page came up. All right, still more of a problem here. Nothing in our cart, because we don't have a cart because we don't have a publication and subscription. So let's do this all again. Have you noticed I like repetition? I find that it's one of the key ways of learning something. You should just keep doing it again and again until it becomes commonplace. So here I'll set up a publication and I'll call it cart. And then it's not going to take any parameters, and I'm simply going to return, and here we go. Carts.find and this is going to accept a userKey, and that'll be the global userKey thing again. I hate that. Notice that I didn't use the method Carts.getCart. The reason for that is because that returns findOne and I need to return a cursor. Instead of putting the subscription down here in the route, however, I'm thinking that later on we're going to want to have access to the cart information all throughout the site, so I'm going to put it up here in the global configuration for the router. To do that, I need to make this an array, and I'm going to say Meteor.subscribe("cart"). This won't work without a userKey, however, so I need to pass along a parameter. So here I would just make sure that I'm accepting a key and the publication. Down below, I'll accept a key in as well. That means up above I'm going to have to pass along that global userKey. This is starting to smell horrible, isn't it? I hate using global variables in this way. Yuck. All right, let's go back and refresh our page, and there is our cart. Our subscription is working as intended. Also notice that if I play with the cart down here in the console, if I do findOne, it's going to return the only cart that I have access to. If I do find.fetch, it will only return that single cart. All right. Let's go back to our receipt page and load it up, but did you notice, we don't have a publication for this, why is it showing up? Do you know why? Well, the simple answer is that I went right around the publication and subscription mechanism when I decided to work with Postgres and use Meteor methods and all that to do this. So there's nothing we need to do for the receipt page. This is all happening outside using NPM and Postgres and Massive. And one last little point of business while we're talking about databases and cleaning things up, I did return to the cart methods there and I switched out the way I was doing saveCart, and what you see here is a method I'm using called updateCart. This simply loops through and makes sure that the only thing a user can do is change a given quantity. Next, I went over to our Meteor settings and I added a database connection string that we can use on the server, and that's what you see here, that is how I'm going to connect to the database, both in development and in production.
I feel like our site is tightened up pretty well, now let's go into production, and for this, we use the MeteorUp package. Can you guess who created it? Yep, Arunoda. So what this thing is going to do is it's going to reach out and configure our server, and it's also going to then deploy our site. It's pretty neat. And to install it is pretty simple, but it is not a Meteor package, this is actually an NPM module that you can install globally. So for this, that's what I'm going to do, I'm going to go and install it globally using npm install mup with a -g, that means global. While that's loading up, let's head back over to the read me on GitHub and we'll read up on how to use MeteorUp. It mentions that you can create a directory for your deployment if you want, and then you just need to run mup in it. That's going to create a mup.json and a settings.json. Since I already have a settings.json file, that means that we just need to tweak the mup.json file, which you see here, this is the default that comes with it. We'll go through all those settings in just a second. After that's done, we just run mup setup. It's going to reach out to our server and install software, and then when we're done with that, mup deploy. Sounds pretty simple. Is it going to work? All right, taking a look here in our directory, you can see I already have our settings.json file. If I run mup init, it's going to tell me that a project already exists, so I think this is a bit silly. It's seeing our settings.json and saying I don't want to overwrite that. Anyway, so let's remove it and I will just refill it after I run mup init. Alternatively, I could have just created a mup.json file instead of being so stubborn. Anyway, here is our mup.json file, and this is the default setting that it wants you to fill out. All of these are pretty self-explanatory and it comes commented pretty well. You need to know where your server lives, if it should set up Mongo or set up Node, and you can set that Node version to whatever version you like, although 0.10.36 is recommended. Specify an app name. You can also specify environment variables if you'd like. Notice the app key right there. I've set it to dot. That is the location of your application. So you don't have to put mup.json into your project directory, although that's where I like to put it. You can put it somewhere else if you'd like. All right, let's head over to Digital Ocean, and let's set up a VM , and this is a critical thing. I have found that my deployments don't go so well unless I have at least 2 GB of RAM. You should have that anyway if this is a production site, but I do want to point that out. IN addition, when selecting the version of Ubuntu, I have found that 13.10 x64 works pretty well. The reason I'm telling you all of these details is that I have had problems working with Meteor Up. In fact, getting it to work at all took me a couple hours the very first time. And the reasons were kind of silly. I'll get into them. But the first reasons were that I didn't have compatible versions of Node and I didn't have compatible versions of Ubuntu. So as you've seen me set up here, go with 14.10, make sure you get 2 GB of RAM, and you should be good to go. Okay, so now that my VM is set up, I'm going to take my IP address and head over to DNSimple, and set up an A record here for our brand-new site, and I do that simply by going to shop, and this is for the bigmachine.io domain, and I'll set the time to life to be one minute, and I'll add the record, there we go. All right, now that that is done, I now have a URL shop.bigmachine.io, excellent. Let's SSH into the box and make sure that everything is okay. I don't know why it wouldn't be, but why not? Let's just check and be sure. So, since Digital Ocean already has SSH key here on file up there, whenever I create a new VM and I specify an SSH key for a given machine, it will pop that right onto the machine itself, so I can just log in directly as you see. Great. Well, it looks like everything is order, the machine seems to be functioning properly, from what I could tell in the login screen. Let's run mup setup right now, and I have told mup.json to look at shop.bigmachine.io. I have set the credentials to use my local SSH key, and it's going to reach out, log into the machine, and it's going to, as you see here, install Node, it's installing Phantom, it's going to install Mongo, and rather than sitting here and watch this all go off, let's fast forward a little bit, and as you can see our machine is set up, boom, with all the stuff that we need. Sort of. We are missing one critical thing, and that is I don't have Postgres on the machine just yet. So let's SSH back into the machine and set that up. First thing I'm going to do is I need to make sure that I do an apt-get update, so it's going to go grab all the new packages and sources and so on. The next thing I need to do, and that's critical by the way, because we're installing Postgres 9.4 I need to do that because the JSONB database is only available in 9.4. Okay, it's installed. It looks like Postgres is running. Now let's create a database, and I need to create the Rocket Shop database, so I'll just use the createdb command that comes with Postgres. And I'll specify rocketshop. And that won't work. There is no username root in the database. Postgres is assuming that since I haven't specified which user, that it's going to be my current username, which is root, and it's going to fail. So the only user that exists now in Postgres is the Postgres user, which is kind of like SA or Admin. I can actually switch users over to that, that was installed when I installed Postgres, so if I do su Postgres, that's switch user, I can do that. Then I just have to jump into my own home root and I can run createdb rocketshop. Good. While I'm still in as the Postgres user, let's go to psql and rocketshop, this will open up a SQL prompt. If you don't know what I'm doing, by the way, I have a Postgres course here on Pluralsight and you can check it out and get up to speed. Okay, first thing I'm doing here is I'm creating a new user called rocket with a password takemetomars, and then I'm granting all the database privileges on the rocketshop to my rocket user. All right, that looks pretty good so far. Our work here appears to be finished. So to jump out of psql, I do \q. Jumping out of su I need to type exit, and then jumping out of ssh I type exit again, and I am logged out. Okay, great. So now we are ready for deployment, believe it or not. Our site's ready, our server's ready, let's hit mup deploy and see what happens. And if you get this to work the very first time, congratulations. This is where I hit a brick wall. Over and over and over I saw so many errors, and it's pretty nondescript, the errors that are splashed on the screen. As you are going to see, when you have an error, it's a good idea to stop and take a look at the logs that mup provides. They are far more informative than any screen splash that you'll see from mup. You'll see what I mean in a second. All right, this deployment process can take a few minutes the very first time, especially if you're on a slow connection, so I'll speed it up a little bit. And it looks like our deployment process was a success. If you get an error here, it's because your Meteor site can't start. That is when you need to go look at the logs, which I'll show you how to do in just a second. All right, let's go check out and see if our site launches. I'll go to shop.bigmachine.io, and it looks like it's coming up. It did come up. Wow! All right, so it did deploy. It looks like Add To Cart is working well, it looks like we hit the checkout page just fine. Let's go and look at the page source just so you can see what our production site looks like, and notice that we don't have all of those script tags anymore. They were concatenated and minified and now we just get one big fat dump, because we are in production, and we don't want to have a ton of script tags go live. Okay, I'll hit Terms and Conditions, let's see if it launches up our Stripe checkout, and it does. My goodness, did we do this correctly? Let's hit our test card again using 4 with a bunch of 1s, I'll put in all the test card data here, and then let's pay and see if everything works. Well, Stripe got the information, okay, and we are stuck on redirection. Hmm, that's not so good. Taking a look at the logs, oh boy. It looks like we get a problem invoking process payment. Well, that's not so good. Hmm. That error splash isn't telling me much. Let's go check the logs. If I type in mup logs it's going to go and grab the application logs for our Meteor deployment, and there it is. Error inside the Async.runSync, cannot call method 'save' of undefined. That's an error that's coming straight off of Massive. Massive works by, as I mentioned before, grabbing all the tables, dropping them onto a single instance of an object, and you can access that table call things like save directly. So, if I call db.checkouts.save, it should save all the stuff down to the checkouts table. In our case, it looks like the checkouts table doesn't exist. Well, let's check our connectionString and make sure it's correct. We have the rocket user and takemetomars is the password, and it's pointing at the rocketshop database. That seems okay, we didn't get a connection error. Hmm. Let's sleuth this a little bit more. Let's open up our SalesRepo and use kind of the old guerilla tactic of console.log. This is going to show up in mup logs. I want to see what my database actually looks like. And I'll leave myself a little message here, The DB is, and there we go, I'll just take a look at the db instance that Massive is working against. All right, so we'll redeploy our application, but first I want to call mup logs -f that's going to tail the logs to the screen. I want to watch that live in action as I deploy my site. So let's hit mup deploy once again. Again, this is going to take just a few minutes. And so fast forwarding a little bit to save some time, there we go. If we head back over to our logs, DB is hmm, right there in the middle where I have queryFiles and schemas and executeSqlFile, right there is tables. That should have a value in it. It should be a string value with the name checkouts, and I don't see it, and for some reason, our checkouts table isn't showing up. Hmm. And as I'm seeing these logs, I'm beginning to wonder, did we even install that table? I feel kind of dumb. All right, let's switch users over to Postgres again, and in here let's change directories to our home directory as the Postgres user, and we'll jump into rocketshop one more time. Let's take a look at the schema in rocketshop using \d. No relations found. Nice, good job, Rob. Nicely done! I completely forgot to install the table into the database. Well, I guess if we don't have a table, we can't really save data to it, can we? Okay, so all I need to do here is just take that create statement and paste it in, not forgetting the semicolon, boom, there is our table. Now that I have done that, let's do a \d again to make sure it's there. It is. That's good. We have our checkouts table and our checkouts_id_seq. Exiting out of here all the way back out, exiting out of SSH as well. It says we didn't really change any code. There's no reason to redeploy our site, we can just use the restart command, and that will reach out to our server and just restart our application. This means we can go and look at the logs again and boom, now comes the logs. Wait a minute. There's no table in here. All right, Postgres fans, I think you probably know what's going on. This is such a silly, ridiculous thing. Can you guess what's happening? If we switch back over to psql and do a \d, describe all the tables, you can see that the owner of our table is Postgres. And our rocket user has no right to go and check out the schema. Oh, geez. So I'll run through grant all and checkouts to rocket again. That should do it. Let's flip back over and then restart our app one more time. And then taking a look at the logs, hopefully everything is good. Oh, yes, look at that! I can see checkouts right there, and I think we're in business. Let's jump back over to our site and run through the checkout process one more time to make sure it all works. I can feel it, I think we're getting close. I'm getting excited; I think this is it, I think this is it! Okay, we're back on the checkout page. Everything looks good. I'll accept those terms and conditions and then let's make sure that Stripe checkout pops up, and it does. I'm going to Mars, I can feel it! All right, let's put in our test card here, 4 with all 1s. It's not like I'm nervous or anything, are you nervous? I'm not nervous. Why would I be nervous? Of course it's going to work. Is it going to work? Let's see. Thank you, redirecting, and bingo! We have a live Meteor based ecommerce site that is working. That's exciting.
I like the idea of having my own server, especially running on Digital Ocean, I love them. However, I really don't like the idea of running MongoDB if I'm going to go into production with it, so for that I would probably use a company like Compose.io. They started around as MongoHQ, and now they have changed their name to Compose.io, and they've reached out to other database platforms, including RethinkDB, Elasticsearch, and so on, but they will manage MongoDB for you and scale it, and I believe they'll do some tuning and you can pay them to come in and test any problems that you might have with it. So if you're going to in production with Meteor, and you know a lot about Mongo, hurray, but if you want someone to look after it for you, I would say that Compose.io is well worth your money. If you want a full turnkey solution, there is Modulus. This is a hosting company that works a little bit like Heroku, you just push your site up there and they deploy it, you can liquid scale it, all those things. Now the one thing that they have that I haven't found in many places is direct support for Meteor. That's pretty neat. You install the Modulus app using NPM install Modulus, and then you use it to deploy your Meteor site, and up it goes to Modulus, and they do all the things that you would expect. They look after your site, make sure it's healthy, and can help you scale it. This is a service that Heroku kind of offers as well. They don't directly support Meteor, but you can run it, and one drawback is that it's not as I said directly supported by Heroku. If you take a look at Heroku Meteor, you can see there's third-party buildpacks, and I guess you can use these, I've never used one, but you can use these to build and deploy your Meteor site if your want. There's a lot of them. As you scroll on down here, you can see Meteor. Frankly, I haven't used Heroku very much. I tried them once or twice with Tech Pub and I didn't have much luck. The last thing I wanted to show you here is the thing that the Modulus people created, which is called the Demeteorizer, I love this. What this does is it basically dismantles your Meteor application and turns it into a regular old Node application, so you could run it anywhere. So the neat thing about this is you just install Demeteorizer and then you tell it to demeteorize your app, and it will. It's sort of a two-step build and deploy process, but all you need is a few clever scripts and off you go, you can run this thing anywhere where Node is supported, including Azure, if you're an Azure fan, or ADWS, et cetera. In this module, we tightened things up by removing autopublish, and we refactored a few things here and there. We dove into pub/sub, latency compensation, and global variables, and we added MeteorUp, or mup, and deployed our site live to Digital Ocean. Then, we checked out different deployment options, such as hosting services, and a neat tool called Demeteorizer.
Final Thoughts and Goodbye
Rob Conery co-founded Tekpub and created This Developer's Life. He is an author, speaker, and sometimes a little bit opinionated.
Released15 Jul 2015