Lesson 21. Capstone: Adding CRUD Models to Confetti Cuisine

Confetti Cuisine is satisfied with the progress I made connecting their application to a database and setting it up to process subscriber information. They’ve sent me a list of cooking courses that they’d like to begin to advertise on their site. Essentially, they want subscribers to pick the courses they’re most interested in attending. Then, if a subscriber later creates a user account, the business wants those two accounts to be linked.

To accomplish this task, I need to improve the Subscriber model and build the User and Course models. I need to keep the relationships between these models in mind and populate data from associated models when necessary. Last, I need to generate all the functionality needed to allow the creation, reading, updating, and deletion (CRUD) of model records. In this capstone, I’m going to create a user login form that allows a user to create an account and then edit, update, and delete the account. I’ll repeat most of the process for courses and subscribers to Confetti Cuisine’s newsletter.

When I’m done, I’ll have an application to show the team at Confetti Cuisine that allows them to sign up new users and monitor their courses before officially launching the program.

For this purpose, I need the following:

  • Schemas for the user, subscriber, and course models
  • CRUD actions for all models in the application
  • Views showing links between models

21.1. Getting set up

Picking up where I left off, I have a MongoDB database connected to my application, with the Mongoose package driving communication between my Subscriber model and raw documents. I’ll need the same core and external packages moving forward. Additionally, I need to install the method-override package to assist with HTTP methods not currently supported by HTML links and forms. I can install this package by running the following code in my project directory in a new terminal window: npm i method-override -S. Then I require the method-override module into main.js by adding const method Override = require("method-override") to the top of the file. I configure the application to use method-override to identify GET and POST requests as other methods by adding the following line: app.use(methodOverride("_method", {methods: ["POST", "GET"]})).

Next, I need to think about how this project’s directory structure will look by the time I’m done. Because I’m adding CRUD functionality to three models, I’m going to create three new controllers, three new folders within views, and three model modules. The structure resembles figure 21.1.

Figure 21.1. Capstone file structure

Notice that I’m creating only four views: index, new, show, and edit. Although delete can have its own view as a deletion confirmation page, I’ll handle deletion through a link on the index page for each model.

Next, I start by improving the Subscriber model and simultaneously build out my User and Course models.

21.2. Building the models

My Subscriber model collected meaningful data for Confetti Cuisine, but they want more security on the data layer. I need to add some validators on the Subscriber schema to ensure that subscriber data meets the client’s expectations before entering the database. My new schema looks like listing 21.1.

I start by requiring Mongoose into this module and pulling the Mongoose Schema object into its own constant. I create my subscriber schema by using the Schema constructor and passing some properties for the subscriber. Each subscriber is required to enter a name and an email address that doesn’t already exist in the database. Each subscriber can opt to enter a five-digit ZIP code. The timestamps property is an add-on provided by Mongoose to record the createdAt and updatedAt attributes for this model.

Each subscriber may subscribe to show interest in multiple courses, so this association allows subscribers to associate with an array of referenced courses. I need to create the Course model for this feature to work. getInfo is an instance method added to the subscriber schema to quickly pull any subscriber’s name, email, and zipCode in one line. Exporting the Subscriber model with this new schema makes it accessible to other modules in the application.

Listing 21.1. Improved Subscriber schema in subscriber.js
const mongoose = require("mongoose"),
  { Schema } = mongoose,                                    1
  subscriberSchema = new Schema({
  name: {                                                   2
    type: String,
    required: true
  },
  email: {
    type: String,
    required: true,
    lowercase: true,
    unique: true
  },
  zipCode: {
    type: Number,
    min: [10000, "Zip code too short"],
    max: 99999
  },
  courses: [{type: Schema.Types.ObjectId, ref: "Course"}]   3
}, {
  timestamps: true
});

subscriberSchema.methods.getInfo = function () {            4
  return `Name: ${this.name} Email: ${this.email}
 Zip Code: ${this.zipCode}`;
};

module.exports = mongoose.model("Subscriber",
 subscriberSchema);                                      5

  • 1 Require mongoose.
  • 2 Add schema properties.
  • 3 Associate multiple courses.
  • 4 Add a getInfo instance method.
  • 5 Export the Subscriber model.

This model looks good, so I’ll apply some of the same techniques to the Course and User model in course.js and user.js, respectively. Every course is required to have a title and description with no initial limitations. A course has maxStudents and cost attributes that default to 0 and can’t be saved as a negative number; otherwise, my custom error messages appear.

The Course schema contains the properties in the following listing.

Listing 21.2. Properties for the Course schema in course.js
const mongoose = require("mongoose"),
  { Schema } = require("mongoose"),
  courseSchema = new Schema(
    {
      title: {                 1
        type: String,
        required: true,
        unique: true
      },
      description: {
        type: String,
        required: true
      },
      maxStudents: {           2
        type: Number,
        default: 0,
        min: [0, "Course cannot have a negative number of students"]
      },
      cost: {
        type: Number,
        default: 0,
        min: [0, "Course cannot have a negative cost"]
      }
    },
    {
      timestamps: true
    }
  );
module.exports = mongoose.model("Course", courseSchema);

  • 1 Require title and description.
  • 2 Default maxStudents and cost to 0, and disallow negative numbers.

The User model contains the most fields and validations because I want to prevent a new user from entering invalid data. This model needs to link to both the Course and Subscriber models. The User schema is shown in listing 21.3.

Each user’s name is saved as a first and last name attribute. The email and zipCode properties behave the same way as in Subscriber. Each user is required to have a password. As for subscribers, users are linked to multiple courses. Because subscribers may eventually create user accounts, I need to link those two accounts here. I also add the timestamps property to keep track of changes to user records in the database.

Listing 21.3. Creating the User model in user.js
const mongoose = require("mongoose"),
  { Schema } = require("mongoose"),
  Subscriber = require("./subscriber"),
  userSchema = new Schema(
    {
      name: {                            1
        first: {
          type: String,
          trim: true
        },
        last: {
          type: String,
          trim: true
        }
      },
      email: {
        type: String,
        required: true,
        unique: true
      },
      zipCode: {
        type: Number,
        min: [10000, "Zip code too short"],
        max: 99999
      },
      password: {
        type: String,
        required: true
      },                                 2
      courses: [
        {
          type: Schema.Types.ObjectId,
          ref: "Course"
        }                                3
      ],
      subscribedAccount: {
        type: Schema.Types.ObjectId,
        ref: "Subscriber"
      }                                  4
    },
    {
      timestamps: true                   5
    }
  );
module.exports = mongoose.model("User", userSchema);

  • 1 Add first and last name attributes.
  • 2 Require password.
  • 3 Associate users with multiple courses.
  • 4 Associate users with subscribers.
  • 5 Add timestamps property.

Two more additions I make to the user model are a virtual attribute to return the user’s full name and a Mongoose pre("save") hook to link subscribers and users with the same email address. Those additions can be added directly below the schema definition in user.js and are shown in listing 21.4.

This first virtual attribute allows me to call fullName on a user to get the user’s first and last names as one value. The pre("save") hook runs right before a user is saved to the database. I’m passing the next parameter so that when this function is complete, I can call the next step in the middleware chain. To link to the current user, I save the user to a new variable outside the scope of my next query. I run the query only if the user doesn’t already have a linked subscribedAccount. I search the Subscriber model for documents that contain that user’s email address. If a subscriber exists, I set the returned subscriber to the user’s subscribedAccount attribute before saving the record and calling the next function in the middleware chain.

Listing 21.4. Adding a virtual attribute and pre(“save”) hook in user.js
userSchema.virtual("fullName").get(function() {        1
  return `${this.name.first} ${this.name.last}`;
});

userSchema.pre("save", function (next) {               2
  let user = this;
  if (user.subscribedAccount === undefined) {          3
    Subscriber.findOne({
      email: user.email
    })                                                 4
      .then(subscriber => {
        user.subscribedAccount = subscriber;
        next();                                        5
      })
      .catch(error => {
        console.log(`Error in connecting subscriber:
 ${error.message}`);
        next(error);
      });
  } else {
    next();
  }
});

  • 1 Add the fullName virtual attribute.
  • 2 Add a pre(‘save’) hook to link a subscriber.
  • 3 Check for a linked subscribedAccount.
  • 4 Search the Subscriber model for documents that contain that user’s email.
  • 5 Call the next middleware function.

With this model set up, I need to build the CRUD functionality. I start by creating the views: index.ejs, new.ejs, show.ejs, and edit.ejs.

21.3. Creating the views

For the Subscriber model, index.ejs lists all the subscribers in the database through an HTML table like the one shown in listing 21.5. This view is a table with five columns. The first three columns display subscriber data, and the last two columns link to edit and delete pages for individual subscribers. For my subscribers index page, I added some new styling (figure 21.2).

Figure 21.2. Subscribers index page in the browser

Note

Because these views have the same names across different models, I need to organize them within separate folders by model name. The views/users folder has its own index.ejs, for example.

To generate a new row for each subscriber, I loop through the subscribers variable, an array of Subscriber objects, and access each subscriber’s attributes. The subscriber’s name is wrapped in an anchor tag that links to that subscriber’s show page by using the user’s _id. The delete link requires the ?_method=DELETE query parameter appended to the path so that my method-override middleware can process this request as a DELETE request. I must remember to close my code block in EJS.

Listing 21.5. Listing subscribers in index.ejs
<h2 class="center">Subscribers Table</h2>
 <table class="table">                                   1
   <thead>
     <tr>
       <th>Name</th>
       <th>Email</th>
       <th>Edit</th>
       <th>Delete</th>
     </tr>
   </thead>
   <tbody>
     <% subscribers.forEach(subscriber => { %>
     <tr>                                                2
      <td>
      <a href="<%= `/subscribers/${subscriber._id}` %>">
        <%= subscriber.name %>                           3
      </a>
      </td>
       <td><%= subscriber.email %></td>
       <td>
       <a href="<%=`subscribers/${subscriber._id}/edit` %>">
         Edit
       </a>
       </td>
       <td>
         <a href="<%=`subscribers/${subscriber._id}/delete?_method=DELETE` %>"
 onclick="return confirm('Are you sure you want to delete this
 record?')">Delete</a>                                4
       </td>
     </tr>
     <% }); %>
   </tbody>
 </table>

  • 1 Add a table to the index page.
  • 2 Generate a new row for each subscriber.
  • 3 Wrap the subscriber’s name in an anchor tag.
  • 4 Add a delete link.

I’ll follow this exact same structure for the course and user index pages, making sure to swap out the variable names and attributes to match their respective models.

With this index page in place, I need a way to create new records. I start with the subscriber’s new.ejs form in listing 21.6. This form will submit data to the /subscribers/- create path, from which I’ll create new subscriber records in the subscriber’s controller. Notice that the form submits data via POST request. Each input reflects the model’s attributes. The name attribute of each form input is important, as I’ll use it in the controller to collect the data I need to save new records. At the end of the form is a submit button.

Listing 21.6. Creating the new subscriber form in new.ejs
<div class="data-form">
  <form action="/subscribers/create" method="POST">       1
    <h2>Create a new subscriber:</h2>
    <label for="inputName">Name</label>
    <input type="text" name="name" id="inputName" placeholder="Name"
 autofocus>
    <label for="inputEmail">Email address</label>
    <input type="email" name="email" id="inputEmail"
 placeholder="Email address" required>
    <label for="inputZipCode">Zip Code</label>
    <input type="text" name="zipCode" id="inputZipCode"
 pattern="[0-9]{5}" placeholder="Zip Code" required>
    <button type="submit">Create</button>
  </form>
</div>

  • 1 Add a form to create new subscribers.

I re-create this form for users and courses, making sure to replace the form’s action and inputs to reflect the model I’m creating. My subscriber edit form looks like the one in figure 21.3.

Figure 21.3. Subscriber edit page in the browser

While I’m working on forms, I create the edit.ejs view, whose form resembles the one in new.ejs. The only changes to keep in mind are the following:

  • The edit form—This form needs access to the record I’m editing. In this case, a subscriber comes from the subscriber’s controller.
  • The form action—This action points to /subscribers/${subscriber._id}/ update?_method=PUT instead of the create action.
  • Attributes—Each input’s value attribute is set to the subscriber variable’s attributes, as in <input type="text" name="name" value="<%= subscriber.name %>">.

These same points apply to the edit.ejs forms for users and courses. The next listing shows my complete subscriber edit page.

Listing 21.7. The edit page for a subscriber in edit.ejs
<form action="<%=`/subscribers/${subscriber._id}/update
 ?_method=PUT` %>" method="POST">                           1
  <h2>Create a new subscriber:</h2>
  <label for="inputName">Name</label>
  <input type="text" name="name" id="inputName" value="<%=
 subscriber.name %>" placeholder="Name" autofocus>
  <label for="inputEmail">Email address</label>
  <input type="email" name="email" id="inputEmail" value="<%=
 subscriber.email %>" placeholder="Email address" required>
  <label for="inputZipCode">Zip Code</label>
  <input type="text" name="zipCode" id="inputZipCode"
 pattern="[0-9]{5}" value="<%= subscriber.zipCode %>"
 placeholder="Zip Code" required>
  <button type="submit">Save</button>
</form>

  • 1 Display the edit form for a subscriber.

Last, I build the show page for each model. For subscribers, this page acts like a profile page, detailing each subscriber’s information in their row on the index page. This page is fairly straightforward: I show enough data to summarize a single subscriber record. The subscribers show page has a table, created with the EJS template elements shown in the following listing. This page uses attributes from a subscriber variable to display the name, email, and zipCode.

Listing 21.8. The show page for a subscriber in show.ejs
<h1>Subscriber Data for <%= subscriber.name %></h1>      1

<table>
  <tr>
    <th>Name</th>
    <td><%= subscriber.name %></td>
  </tr>
  <tr>
    <th>Email</th>
    <td><%= subscriber.email %></td>
  </tr>
  <tr>
    <th>Zip Code</th>
    <td><%= subscriber.zipCode %></td>
  </tr>
</table>

  • 1 Display subscriber attributes.
Note

For some of these views, I’ll add links to navigate to other relevant pages for that model.

Something else I want to add to the show page is code that shows whether the record is associated with any other records in the database. For a user, that code showing an associated record could display using an additional tag at the bottom of the page to show whether the user has a subscribedAccount or associated courses. For subscribers, I’ll add a line to show the number of subscribed courses, as shown in listing 21.9.

This one line gives Confetti Cuisine insight into the number of courses to which people are subscribing. I could take this line a step further by using the Mongoose populate method on this subscriber to show the associated course details.

Listing 21.9. Show the number of subscribed courses in show.ejs
<p>This subscriber has <%= subscriber.courses.length %> associated
 course(s)</p>                                                     1

  • 1 Display the number of associated courses.

The last step is bringing the models and views together with the controller actions and routes.

21.4. Structuring routes

The forms and links for Confetti Cuisine are ready to be displayed, but there’s still no way to reach them via a browser. In main.js, I’m going to add the necessary CRUD routes and require the controllers needed to get everything working.

First, I add the routes for subscribers from listing 21.10 to main.js. To make sure that the subscribersController is required near the top of the file alongside my other controllers, I add const subscribersController = require("./controllers/subscribersController"). I also introduce the Express.js Router to my project to help distinguish application routes from other configurations in main.js by adding const router = express.Router(). With this router object in place, I change every route and middleware handled by my app object to use the router object. Then I tell my application to use this router object by adding app.use("/", router) to main.js.

GET requests to the /subscribers path lead me to the index action on the subscribers-Controller. Then I render the index.ejs page through another action called indexView. The same strategy applies to the other GET routes. The first POST route is for create. This route handles requests from forms to save new subscriber data. l need to create the logic to save new subscribers in the create action. Then I use an action called redirectView that will redirect to one of my views after I successfully create a subscriber record.

The show route is the first case in which I need to get the subscriber’s ID from the path. In this case, :id represents the subscriber’s ObjectId, allowing me to search for that specific subscriber in the database in the show action. Then I use a showView to display the subscriber’s data in a view. The update route works like the create route, but I’m specifying the router to accept only PUT requests, indicating that a request is being made specifically to update an existing record. Similarly, I use the redirectView action after this to display a view. The last route, delete, accepts only DELETE requests. Requests will be made from the link on index.ejs and use the redirectView to link back to the index page.

Listing 21.10. Adding subscriber CRUD routes to main.js
router.get("/subscribers", subscribersController.index,
 subscribersController.indexView);                                1
router.get("/subscribers/new", subscribersController.new);
router.post("/subscribers/create", subscribersController.create,
 subscribersController.redirectView);                             2
router.get("/subscribers/:id", subscribersController.show,
 subscribersController.showView);                                 3
router.get("/subscribers/:id/edit", subscribersController.edit);
router.put("/subscribers/:id/update", subscribersController.update,
 subscribersController.redirectView);                             4
router.delete("/subscribers/:id/delete",
 subscribersController.delete,
 subscribersController.redirectView);                             5

  • 1 Add GET routes to show views.
  • 2 Add the first POST route for create.
  • 3 Add a route to show a subscriber based on ObjectId.
  • 4 Add a route to update subscribers.
  • 5 Add a route to delete subscribers.

The same seven routes need to be made for users and courses. I’ll also update the navigation links: the contact link will point to the subscribers’ new view, and the courselistings link will point to the courses’ index view.

Note

At this point, I can remove some of my deprecated routes, such as the ones that point to getAllSubscribers, getSubscriptionPage, and saveSubscriber in the subscribers controller, as well as showCourses in the home controller. I can also move my home-page route to the home controller’s index action. Finally, I want to make sure that I update my navigation links to point to /subscribers/new instead of /contact.

All I have left to do is create the corresponding controllers.

21.5. Creating controllers

The routes I created in main.js require a subscribersController, coursesController, and usersController. I start by creating those files in the controllers folder.

Note

I’ve also cleaned up my error controller to use http-status-codes and an error.ejs view, as in previous application examples.

Next, for the subscriber’s controller, I add the actions shown in listing 21.11 to handle requests made to my existing routes. After requiring the Subscriber model into this file, I create the index action to find all subscriber documents in my database and pass them through the subscribers variable into index.ejs via the indexView action. The new and edit actions also render a view to subscribe and edit subscriber data.

The create action collects request body parameters in my custom getSubscriberParams function, listed as the second constant in the code listing, to create a new subscriber record. If I’m successful, I’ll pass the user object through the locals variables object in my response. Then I’ll specify to redirect to the index page in the redirectView action.

The show action pulls the subscriber’s ID from the URL with req.params.id. This value is used to search the database for one matching record and then pass that record to the next middleware function through the response object. In showView, the show page displays the contents of this subscriber variable. The update action behaves like create and uses the findByIdAndUpdate Mongoose method to set new values for an existing subscriber document. Here, I also pass the updated user object through the response object and specify a view to redirect to in the redirectView action.

The delete action uses the subscriber’s ID in the request parameters to findByIdAndRemove a matching document from the database. The getSubscriberParams function is designed to have less repetition in my code. Because the create and update actions use form parameters, they can call this function instead. The redirectView action is also intended to reduce code repetition by allowing multiple actions, including the delete action, to specify what view to render when the main function is complete.

Listing 21.11. Adding subscriber controller actions in subscribersController.js
const Subscriber = require("../models/subscriber"),
  getSubscriberParams = (body) => {                      1
    return {
      name: body.name,
      email: body.email,
      zipCode: parseInt(body.zipCode)
    };
  };

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

  new: (req, res) => {
    res.render("subscribers/new");
  },

  create: (req, res, next) => {                          3
    let subscriberParams = getSubscriberParams(req.body);
    Subscriber.create(subscriberParams)
      .then(subscriber => {
        res.locals.redirect = "/subscribers";
        res.locals.subscriber = subscriber;
        next();
      })
      .catch(error => {
        console.log(`Error saving subscriber:${error.message}`);
        next(error);
      });
  },

  redirectView: (req, res, next) => {
    let redirectPath = res.locals.redirect;
    if (redirectPath) res.redirect(redirectPath);
    else next();
  },
  show: (req, res, next) => {                            4
    var subscriberId = req.params.id;
    Subscriber.findById(subscriberId)
      .then(subscriber => {
        res.locals.subscriber = subscriber;
        next();
      })
      .catch(error => {
        console.log(`Error fetching subscriber by ID:
 ${error.message}`)
        next(error);
      });
  },

  showView: (req, res) => {
    res.render("subscribers/show");
  },

  edit: (req, res, next) => {
    var subscriberId = req.params.id;
    Subscriber.findById(subscriberId)
      .then(subscriber => {
        res.render("subscribers/edit", {
          subscriber: subscriber
        });
      })
      .catch(error => {
        console.log(`Error fetching subscriber by ID:
 ${error.message}`);
        next(error);
      });
  },

  update: (req, res, next) => {                         5
    let subscriberId = req.params.id,
      subscriberParams = getSubscriberParams(req.body);

    Subscriber.findByIdAndUpdate(subscriberId, {
      $set: subscriberParams
    })
      .then(subscriber => {
        res.locals.redirect = `/subscribers/${subscriberId}`;
        res.locals.subscriber = subscriber;
        next();
      })
      .catch(error => {
        console.log(`Error updating subscriber by ID:
 ${error.message}`);
        next(error);
      });
  },

  delete: (req, res, next) => {                         6
    let subscriberId = req.params.id;
    Subscriber.findByIdAndRemove(subscriberId)
      .then(() => {
        res.locals.redirect = "/subscribers";
        next();
      })
      .catch(error => {
        console.log(`Error deleting subscriber by ID:
 ${error.message}`);
        next();
      });
  }
};

  • 1 Create a custom function to pull subscriber data from the request.
  • 2 Create the index action to find all subscriber documents.
  • 3 Create the create action to create a new subscriber.
  • 4 Create the show action to display subscriber data.
  • 5 Create the update action to set new values for an existing subscriber document.
  • 6 Create the delete action to remove a subscriber document.

With these controller actions in place for each model, the application is ready to boot and manage records. I load the views for each model and then create new subscribers, courses, and users. In unit 5, I improve Confetti Cuisine’s site by adding user authentication and a login form.

Summary

In this capstone exercise, I improved Confetti Cuisine’s application by adding CRUD functionality to three new models. These models allow subscribers to sign up for Confetti Cuisine’s upcoming course offerings and create user accounts to get involved with the cooking class product. In unit 5, I clean up these views by adding flash messaging, password security, and user authentication with the passport module.

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

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