Lesson 18. Building the user model

In lesson 17, you improved your models by adding validators and instance methods. You also made your first model associations and populated data from referenced models. In this lesson, you apply some of those techniques to the user model. In doing so, you also interact with these models through their respective controllers and routes. Last, you build some forms and tables to make it easier to visualize all the data in the application.

This lesson covers

  • Creating model associations with a user model
  • Using virtual attributes
  • Implementing a CRUD structure on the user model
  • Building an index page to view all users in your database
Consider this

You have two models working with your recipe application: Subscriber and Course. You’d still like visitors to create accounts and start signing up for recipe programs. The user model is in nearly every modern application, along with a system to create, read, update, and delete (CRUD) data from the database. With the help of Mongoose, Express.js, and CRUD, your users will soon have a way to sign in to your application.

18.1. Building the user model

Now that you have models that protect against unwanted data in your database, you need to do the same for the most important model in the application: user. Your recipe application currently has a subscriber model and a course model to allow prospective users to show interest in certain recipe programs. The next step is allowing users to sign up for and enroll in these courses.

Like the subscriber model, the user model needs some basic information about each person who signs up. The model also needs an association with the course and subscriber models. (If a former subscriber decides to sign up as a user, for example, you want to connect the two accounts.) Then you want to track all the courses in which the user decides to participate.

To create the user model, add the code in listing 18.1 to a new file in your models folder called user.js. The user schema contains many overlapping properties from the subscriber schema. Instead of a name property that’s one String, here, the name is an object containing first and last. This separation can help if you want to address the user by first name or last name only. Notice that the trim property is set to true to make sure that no extra whitespace is saved to the database with this property. Email and zipCode are the same as in the subscriber schema. The password property currently stores the user’s password as a string and is required before an account is created.

Warning

For this unit only, you’ll be saving passwords to the database in plain text. This approach isn’t secure or recommended, however, as you’ll learn in unit 5.

As in the subscriber schema, you associate the user to many courses. The user may also be connected to a single subscriber’s account. You can name the property subscribed-Account and remove brackets to signify that only one object is associated. A new set of properties, createdAt and updatedAt, populates with dates upon the creation of a user instance and any time you change values in the model. The timestamps property lets Mongoose know to include the createdAt and updatedAt values, which are useful for keeping records on how and when data changes. Add the timestamps property to the subscriber and course models, too.

Note

Notice the use of object destructuring for the Mongoose Schema object. {Schema} assigns the Schema object in mongoose to a constant by the same name. Later, you’ll apply this new format to other models.

Listing 18.1. Creating a User model in user.js
const mongoose = require("mongoose"),
  {Schema} = mongoose,

  userSchema = new Schema({                                  1
  name: {                                                    2
    first: {
      type: String,
      trim: true
    },
    last: {
      type: String,
      trim: true
    }
  },
  email: {
    type: String,
    required: true,
    lowercase: true,
    unique: true
  },
  zipCode: {
    type: Number,
    min: [1000, "Zip code too short"],
    max: 99999
  },
  password: {
    type: String,
    required: true
  },                                                         3
  courses: [{type: Schema.Types.ObjectId, ref: "Course"}],   4
  subscribedAccount: {type: Schema.Types.ObjectId, ref:
 "Subscriber"}                                            5
}, {
  timestamps: true                                           6
});

  • 1 Create the user schema.
  • 2 Add first and last name properties.
  • 3 Add a password property.
  • 4 Add a courses property to connect users to courses.
  • 5 Add a subscribedAccount to connect users to subscribers.
  • 6 Add a timestamps property to record createdAt and updatedAt dates.

Given that the first and last name may occasionally be useful in one line, you can use a Mongoose virtual attribute to store that data for each instance. A virtual attribute (also known as a computed attribute) is similar to a regular schema property but isn’t saved in the database. To create one, use the virtual method on your schema, and pass the property and new virtual attribute name you’d like to use. A virtual attribute for the user’s full name resembles the code in listing 18.2. This virtual attribute won’t be saved to the database, but it will behave like any other property on the user model, such as user.zipCode. You can retrieve this value with user.fullName. Below that is a line to create the user model.

Listing 18.2. Adding a virtual attribute to the user model in user.js
userSchema.virtual("fullName")
  .get(function() {
    return `${this.name.first} ${this.name.last}`;
  });                                                 1

module.exports = mongoose.model("User", userSchema);

  • 1 Add a virtual attribute to get the user’s full name.
Note

As of the writing of this book, you won’t be able to use arrow functions here because Mongoose methods use lexical this, on which ES6 arrow functions no longer depend.

Test this model right away in REPL. Remember to require Mongoose and everything needed for this environment to work with your new model. With a new REPL session, you need to require Mongoose again, specify Mongoose to use native promises, and connect to your database by typing mongoose.connect("mongodb://localhost:27017/recipe_db", {useNewUrlParser: true}). Then require the new user model with const User = require ("./models/user").

Create a new user instance in REPL, and log the returned user or error to see whether the model was set up correctly. Listing 18.3 shows a working line to create a sample user. In this example, a user is created and saved to the database with all the required properties. Notice the extra space in the last field, which should be trimmed through Mongoose before saving to the database.

Tip

You can add the REPL commands in these examples to your REPL.js file for future use.

Listing 18.3. Creating a new user in REPL in terminal
var testUser;
User.create({
  name: {
    first: "Jon",
    last: "Wexler"
  },
  email: "[email protected]",
  password: "pass123"
})
  .then(user => testUser = user)
  .catch(error => console.log(error.message));         1

  • 1 Create a new user.
Note

If you get an error complaining about unique email addresses, it probably means that you’re trying to create a user with the same information as one in your database (which isn’t permitted, due to the rules you set in the user schema). To get around this restriction, create a user with a different email address or use the find() method instead of create, like so: User.findOne({email: "[email protected]"}).then(u=> testUser = u) .catch(e => console.log(e.message));.

The user variable should now contain the document object shown in the next listing. Notice that the courses property for this user is an empty array. Later, when you associate this user with courses, that property will populate with ObjectIds.

Listing 18.4. Showing the results of a saved user object in terminal
{ _id: 598a3d85e1225d0bbe8d88ae,
  email: "[email protected]",
  password: "pass123",
  __v: 0,
  courses: [],
  name: { first: "Jon", last: "Wexler" } }      1

  • 1 Display of query response

Now you can use the information from this user to link any subscribers in the system with the same email. To link a subscriber, see the code in listing 18.5. You’re setting up a targetSubscriber variable scoped outside of the query and assigning it the results of the query on the subscriber model. This way, you can use your targetSubscriber variable after the query completes. In this query, you’re using the user’s email from the create command earlier to search over subscribers.

Listing 18.5. Connecting a subscriber to the user in REPL in terminal
var targetSubscriber;
Subscriber.findOne({
    email: testUser.email
  })
  .then(subscriber => targetSubscriber = subscriber);       1

  • 1 Set the targetSubscriber variable to a subscriber found with the user’s email address.

After you run these commands, your targetSubscriber variable should contain the value of the subscriber object that shares the user’s email address. You can console.log(target Subscriber); to see that content in your REPL environment.

With promises, you can condense these two operations into one, as shown in listing 18.6. By nesting the call to find associated subscribers, you get a promise chain that can be moved as a whole into a controller action. First, create the new user. You get back the new user whose email you use to search for subscribers with the same email. The second query returns any subscribers that exist. When you find the subscriber with the same email, you can link it with the user by its attribute name on the user model, subscribedAccount. Finally, save the change.

Listing 18.6. Connecting a subscriber to the user in REPL in terminal
var testUser;
User.create({
  name: {
    first: "Jon",
    last: "Wexler "
  },
  email: "[email protected]",
  password: "pass123"
})
  .then(user => {
    testUser = user;
    return Subscriber.findOne({
      email: user.email
    });                                                            1
  })
  .then(subscriber => {
    testUser.subscribedAccount = subscriber;                       2
      testUser.save().then(user => console.log("user updated"));
  })
  .catch(error => console.log(error.message));

  • 1 Find a subscriber with the user’s email.
  • 2 Connect a subscriber and user.

Now that you can create a user and connect it to another model in REPL, the next step is moving this interaction to the controllers and views.

Note

You’ve moved to REPL to test your database queries, so you can remove the required subscriber module from main.js, where it’s no longer needed.

Quick check 18.1

Q1:

How are virtual attributes different from normal model attributes?

QC 18.1 answer

1:

Virtual attributes aren’t saved in the database. These attributes, unlike normal schema attributes, exist only while the application is running; they can’t be extracted from the database or found directly through MongoDB.

 

18.2. Adding CRUD methods to your models

In this section, I discuss the next steps you need to take with the user, subscriber, and group models. All three models have schemas and associations that work in REPL, but you’re going to want to use them in the browser. More specifically, you want to manage the data for each model as an admin of the site and allow users to create their own user accounts. First, I’ll talk about the four major functions in database operations: create, read, update, and delete (CRUD). Figure 18.1 illustrates these functions.

Figure 18.1. Views for each CRUD action

In web development, a CRUD application lays the groundwork for any larger or more evolved application, because at the root and in some way, you always need to perform the actions listed in table 18.1 on each model.

Table 18.1. CRUD actions

Action

Description

Create The create function has two parts: new and create. new represents the route and action taken to view the form with which you’ll create a new instance of your model. To create a new user, for example, you might visit http://localhost:3000/users/ new to view a user-creation form located in new.ejs. The create route and action handle any POST requests from that form.
Read The read function has only one route, action, and view. In this book, their names are show to reflect that you’re showing that model’s information, most likely as a profile page. Although you’re still reading from the database, the show action and show.ejs view are more conventional names used for this operation.
Update The update function has two parts: edit and update. edit, like new, handles GET requests to the edit route and edit.ejs view, where you’ll find a form to change a model’s attribute values. When you modify the values and submit the form by using a PUT request, the update route and action handle that request. These functions depend on some instance of the model preexisting in your database.
Delete The delete function can be the simplest of the functions. Although you can create a view to ask a user whether he’s sure that he wants to delete a record, this function is usually implemented with a button that sends a DELETE request to a route with a user’s ID. Then the delete route and action remove the record from your database.

For the new.ejs and edit.ejs forms, you need to route the form submissions to create and update routes, respectively. When you submit a form to create a new user, for example, the form data should be posted to the user/create route. The following examples walk you through the creation of CRUD actions and views for the user model, but you should apply the same technique to the course and subscriber models.

CRUD HTTP methods

Earlier in this book, you learned about the GET and POST HTTP methods, which account for most of the requests made across the internet. Many other HTTP methods are used in specific cases, and with the update and delete functions, you can introduce two more, as shown in table 18.2.

Table 18.2. PUT and DELETE HTTP methods

HTTP method

Description

PUT The method used to indicate that you’re submitting data to the application server with the intention of modifying or updating an existing record. PUT usually replaces the existing record with a new set of attributes, even if some haven’t changed. Although PUT is the leading method for updating records, some people prefer the PATCH method, which is intended to modify only the attributes that have changed. To handle update routes in Express.js, you can use app.put.
DELETE The method used to indicate that you’re removing a record from your database. To handle delete routes in Express.js, you can use app.delete.

Although you can get away with using GET and POST to update and delete records, it’s best to follow these best practices when using HTTP methods. With consistency, your application will run with fewer problems and better transparency when problems arise. I discuss these methods further in lesson 19.

Before you get started, take a look at your controllers, and prepare them for a renovation. So far, you’ve created new controller actions by adding them to the module’s exports object. The more actions you create, the more you repeat that exports object, which isn’t particularly pretty in the controller module. You can clean up your controller actions by exporting them all together with module.exports in an object literal. Modify your home controller to the code in listing 18.7.

In this example, your actions are now comma-delimited, which makes the names of the actions much easier to identify. After you apply this change in the controller, you don’t need to change any other code for the application to function as it did before.

Listing 18.7. Modifying your actions in homeController.js
var courses = [
  {
    title: "Event Driven Cakes",
    cost: 50
},
  {
    title: "Asynchronous Artichoke",
    cost: 25
},
  {
    title: "Object Oriented Orange Juice",
    cost:10
}];

module.exports = {                   1
  showCourses: (req, res) => {
    res.render("courses", {
      offeredCourses: courses
    });
  }
};

  • 1 Export object literal with all controller actions.

Apply this structure to your other controllers (errorController.js and subscribers-Controller.js) and to all controllers moving forward. These modifications will start to become important as you build out your CRUD actions and structure your middleware within your routes.

Note

Also create coursesController.js and usersController.js in your controllers folder so that you can create the same actions for the course and user models over the next few lessons.

In the next section, you build the forms you need for the user model. First, though, create an often-overlooked view for the application: index.ejs. Also create this index page for each application model. The purpose of the index route, action, and view is to fetch all records and display them on a single page. You build the index page in the next section.

Quick check 18.2

Q1:

What CRUD functions don’t necessarily need a view?

QC 18.2 answer

1:

Although every CRUD function can have its own view, some functions could live in modals or be accessed through a basic link request. The delete function doesn’t necessarily need its own view because you’re sending a command to delete a record.

 

18.3. Building the index page

To start, create the index.ejs view by creating a new users folder inside the views folder and adding the code in listing 18.8.

In this view, you’re looping through a users variable and creating a new table row listing each user’s attributes. The same type of table can be used for subscribers and courses. You need to populate the users variable with an array of users at the controller level.

Note

You should apply the same approach to other models in your application. The subscriber model views will now go in the subscribers folder within the views folder, for example.

Listing 18.8. Listing all users in index.js
<h2>Users Table</h2>
 <table class="table">
   <thead>
     <tr>
       <th>Name</th>
       <th>Email</th>
       <th>Zip Code</th>
     </tr>
   </thead>
   <tbody>
     <% users.forEach(user => { %>           1
     <tr>
       <td><%= user.fullName%></td>
       <td><%= user.email %></td>
       <td><%= user.zipCode%></td>
     </tr>
     <% }); %>
   </tbody>
 </table>

  • 1 Loop through an array of users in the view.

To test this code, you need a route and controller action that will load this view. Create a usersController.js in the controllers folder with the code in listing 18.9.

You need to require the user model in usersController.js to have access to it in this controller. First, you receive a response from the database with your users. Then you render your list of users in your index.ejs view. If an error occurs, log the message to the console and redirect the response to the home page.

Listing 18.9. Creating the index action in usersController.js
const User = require("../models/user");      1

module.exports = {
  index: (req, res) => {
    User.find({})
      .then(users => {                       2
          res.render("users/index", {
            users: users
          })
      })
      .catch(error => {                      3
          console.log(`Error fetching users: ${error.message}`)
        res.redirect("/");
      });
  }
};

  • 1 Require the user model.
  • 2 Render the index page with an array of users.
  • 3 Log error messages and redirect to the home page.
Note

In the subscribers controller, the index action replaces your getAllSubscribers action. Remember to modify the action’s corresponding route in main.js to point to index and to change the subscribers.ejs file to index.ejs. This view should now live in a subscribers folder within views.

The last step is introducing the usersController to main.js and adding the index route by adding the code in listing 18.10 to main.js.

First, require the usersController into main.js. Add this line below where your subscribers-Controller is defined. Creating your first user route, take incoming requests to /users, and use the index action in usersController.

Listing 18.10. Adding usersController and a route to main.js
const usersController = require("./controllers/usersController");  1
app.get("/users", usersController.index);                          2

  • 1 Require usersController.
  • 2 Create the index route.

Fire up your application in terminal, and visit http://localhost:3000/users. Your screen should resemble figure 18.2.

Figure 18.2. Example of users index page in your browser

This list is your window into the database without revealing any sensitive data to the public. Before you continue, though, make one more modification to your routes and actions.

Quick check 18.3

Q1:

What is the purpose of the index view?

QC 18.3 answer

1:

The index view displays all documents for a particular model. This view can be used internally by a company to see the names and email addresses of everyone who subscribed. It can also be visible to all users so that everyone can see who signed up.

 

18.4. Cleaning up your actions

Right now, your index action is designed to serve only an EJS template view with data from your database. You may not always want to serve your data in a view, however, as you learn in unit 6. To make better use of your actions, break them into an action to run your query and an action to serve results through your view.

Modify the users controller to the code shown in listing 18.11. In this revised code, you have the index action, which calls the find query on the user model. If you successfully produce results, add those results to the res.locals object—a unique object on the response that lets you define a variable to which you’ll have access in your view. By assigning the results to res.locals.users, you won’t need to change your view; the variable name users matches locally in the view. Then call the next middleware function. If an error occurs in the query, log the error, and pass it to the next middleware function that will handle the error. In this case, that function is the internalServerError action in the errors controller. The indexView action renders the index view.

Listing 18.11. Revisiting the index action in usersController.js
const User = require("../models/user");

module.exports = {
  index: (req, res, next) => {
    User.find()                                                 1
      .then(users => {
        res.locals.users = users;                               2
          next();
      })
      .catch(error => {
        console.log(`Error fetching users: ${error.message}`);
        next(error);                                            3
      });
  },
  indexView: (req, res) => {
    res.render("users/index");                                  4
  }
};

  • 1 Run query in index action only.
  • 2 Store the user data on the response and call the next middleware function.
  • 3 Catch errors, and pass to the next middleware.
  • 4 Render view in separate action.

To get your application to load your users on the index page as before, add the indexView action as the middleware function that follows the index action in your route. To do so, change the /users route in main.js to the following code: app.get("/users", usersController.index, usersController.indexView). When usersController.index completes your query and adds your data to the response object, usersController.indexView is called to render the view. With this change, you could later decide to call a different middleware function after the index action in another route, which is exactly what you’ll do in unit 6.

Now you have a way, other than REPL and the MongoDB shell, to view the users, courses, and subscribers in your database. In lesson 19, you pull more functionality into the views.

Quick check 18.4

Q1:

Why do you need to log error messages to the console if you’re working mainly in the browser?

QC 18.4 answer

1:

Although you’re moving more data and functionality into the views, your terminal is still the heart of your application. Your console window is where you should expect to see application errors, requests made, and custom error messages you create so that you’ll know where to look to fix the problem.

 

Summary

In this lesson, you learned how to create a user model and where to get started with CRUD functions. You also learned about two new HTTP methods and saw how to create an index page to display all your users. With this index page, you started to interact with your application from the browser. Finally, you modified your controller and routes to make better use of middleware functions and interactivity among your actions. In lesson 19, you apply the create and read functions to your three models.

Try this

With your index page set up, try to think about how an administrator of your application might use this page. You created the table to display user data, but you may want other columns in this table. Create new user instance methods to give you the number of characters in each user’s name and then create a new column in this table to show that number for each user.

Try creating a new virtual attribute for the user model.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset