Chapter 5: Book data model. Chapter data model. MongoDB index. API infrastructure and user roles. Read chapter API.
The section below is a preview of Builder Book. To read the full text, you will need to purchase the book.
In Chapter 5, you'll start with the codebase in the 5-begin folder of our builderbook repo and end up with the codebase in the 5-end folder. We'll cover the following topics in this chapter:
Book data model - Schema for Book data model - Static methods for Book data model
Chapter data model - Schema for Chapter data model - Static methods for Chapter data model
MongoDB index
API infrastructure and user roles - Pages and components for user roles - API methods by user roles - Express routes by user roles - Custom routing for pages
Read chapter API - ReadChapter page - Testing Read chapter API
In the previous chapter (Chapter 4), you learned how to test methods with Jest, integrated our project with AWS SES to send transactional emails, and created a Notifier component and notify method to show users informational messages (such as success or error messages).
In this chapter (Chapter 5), we introduce the Book and Chapter data models. You will be able to sell a book once you paywall your chapters' content and add Stripe API infrastructure in Chapter 8. In addition, we will discuss related API methods and Express routes define a new page, the ReadChapter page.
As discussed in the Introduction chapter, our project has three types of users - an Admin user who writes a book, a Customer who buys and reads a book, and a Public user (a logged-out user).
In this chapter, we will build the ReadChapter page that is for both Public and Customer users. We will make many improvements to the ReadChapter page in Chapter 7. In Chapter 6, we will build four Admin-related pages. Also in Chapter 6, we will build a MyBooks page for a Customer user. We will improve this page further in Chapter 8.
Book data model link
In this section, we introduce the Book data model. We will define this data model, schema, and a few static methods. We will eventually add more static methods and update schema for Github API (Chapter 6) and Stripe API (Chapter 8).
At this point in the book, you've already successfully created User and EmailTemplate data models. When working on the User data model, you learned how to define Mongoose's schema and data model. To summarize:
const { Schema } = mongoose;
const mongoSchema = new Schema({
// parameters
});
const Book = mongoose.model('Book', mongoSchema);You also learned, in Chapter 3, how to add static methods to a data model by using class and loadClass:
class BookClass {
// static methods
};
mongoSchema.loadClass(BookClass);We typically call these static methods inside corresponding Express routes.
Based on what you learned, we can create the Book data model in two steps: - Discuss and add parameters to the Book schema (parameters such as name and price). These parameters correspond to fields of the Book MongoDB document and to properties of the book JS object. - Discuss and write some static methods, then add them to the the Book's class, BookClass (methods such as add and edit), and add them to the data model with the loadClass method.
According to Mongoose docs, we have to call mongoSchema.loadClass to create an ES6 class from which Mongoose Schema will be created:
https://mongoosejs.com/docs/api.html#schema_Schema-loadClass
Thus the line:
mongoSchema.loadClass(BookClass);The static methods we defined are ES6 class and will be inherited by the Book Mongoose Model (data model) after we create it by calling mongoose.model.
Here's the blueprint (bare model) of the Book data model (it's similar to any other data model, like our User data model): server/models/Book.js :
const mongoose = require('mongoose');
const { Schema } = mongoose;
const mongoSchema = new Schema({
// parameters/fields/properties
});
class BookClass {
// static methods
}
mongoSchema.loadClass(BookClass);
const Book = mongoose.model('Book', mongoSchema);
module.exports = Book;Schema for Book data model link
For our Book data model, we want at least four parameters: name (important for slug generation and SEO), slug (generated from name, important for URL queries and for the Book.getBySlug static method), createdAt (generally a good idea to have, you can use it to sort books by data), price (the price a Customer user will pay to access paywalled content):
const { Schema } = mongoose;
const mongoSchema = new Schema({
name: {
type: String,
required: true,
},
slug: {
type: String,
required: true,
unique: true,
},
createdAt: {
type: Date,
required: true,
},
price: {
type: Number,
required: true,
},
});Eventually, we will add more parameters to the User schema, when we work on Github- and Stripe-related API infrastructures.
Static methods for Book data model link
Alright, we are done with the User schema for now. Next, let's define static methods for the BookClass class.
You learned about Mongoose's class properties in Chapter 3 when we discussed UserClass and added the static methods User.publicFields (returns an array of public data) and User.signInOrSignUp (either finds an existing User MongoDB document and returns a corresponding user object or creates a new User MongoDB document and returns a corresponding user object).
We will start with four static methods for BookClass (and eventually add more in the next chapters, for example Book.syncContent for Github integration and Book.buy for Stripe integration):
Book.liststatic method finds allBookMongoDB documents and returns an object with abooksproperty, which is an array of correspondingbookobjects. We can use this method in the future to show all books to an Admin user at theAdminpage - more on this in Chapter 6. 2.Book.getBySlugstatic method finds one uniqueBookMongoDB document by itsslug, then returns the correspondingbookobject. We'll use it to display a single book, for example, when a Public or Customer user loads theReadChapterpage ( more on this later in this chapter). 3.Book.addcreates a newBookMongoDB document. We'll call it, indirectly, from theAddBookpage in Chapter 6. This method returns a correspondingbookobject. 4.Book.editfinds aBookMongoDB document and updates some of its fields. This method is called by an Admin user indirectly from theEditBookPagepage in Chapter 6. This method returns a correspondingbookobject.
To summarize what we just discussed about the four static methods, we can write:
class BookClass {
static async list({ offset = 0, limit = 10 } = {}) {
// some code
// returns object with property books which is array of up to 10 members
}
static async getBySlug({ slug }) {
// some code
// returns book object
}
static async add({ name, price, githubRepo }) {
// some code
// returns newly created book object
}
static async edit({ id, name, price, githubRepo }) {
// some code
// returns updated book object
}
}
mongoSchema.loadClass(BookClass);Let's define each of the above four static methods.
The static and async
Book.listmethod (static async list(...)) takes two arguments:offsetandlimit. The method waits (await) until all books are found with thefindMongoose API method and returns an array of book objects as thebooksproperty of the object:return { books }Inside the
Book.listmethod, we use three MongoDB methods to reorganize the array of book objects: sort, skip, limit:static async list({ offset = 0, limit = 10 } = {}) { const books = await this.find({}) .sort({ createdAt: -1 }) .skip(offset) .limit(limit); return { books }; }.sort({ createdAt: -1 })sorts book objects by creation date, from the most to least recently created..skip(offset)withoffset = 0ensures that we do not skip any books..limit(limit)andlimit=10returns no more than 10 books. If we return too many books, MongoDB's query time may be high and user-unfriendly.
The default value for theskipmethod is zero, so we don't need to specify it explicitly. However, let's keep theoffsetargument explicit so there is an easy way to modify its value in the future if needed. For example, we can modify the value foroffsetif we decide to add pagination to our list of books.
We discussed theasync/awaitconstruct in detail in Chapter 3. We useasync/awaitfor practically all static methods of all data models.The static and async
Book.getBySlugmethod (static async getBySlug(...)) takes one argument:slug. The main method waits (await) until Mongoose's API methodfindOnefinds one book (slugis unique, meaning allBookMongoDB documents must have a unique value for theslugfield):slug: { type: String, required: true, unique: true, },It's always a good habit to check for edge cases. What if a
BookMongoDB document cannot be found? Then the corresponding object is not truthy, and we should throw some informative error:throw new Error('Book not found');If truthy, we take the book object returned from the database server and convert it into a plain JavaScript object by using Mongoose's toObject method:
const book = bookDoc.toObject();In summary, we have the following for the
Book.getBySlugmethod:static async getBySlug({ slug }) { const bookDoc = await this.findOne({ slug }); if (!bookDoc) { throw new Error('Book not found'); } const book = bookDoc.toObject(); return book; }When any user of our web application loads the
ReadChapterpage, we want to display a chapter's content. We need thebook.chaptersproperty to be populated with an array of a book's chapters -chapterobjects that correspond toChapterMongoDB documents. To do so, we can call theChapter.findMongoose API method that returns an array of objects and usesbook._idas search criteria. Then for every member of this array, we can run Mongoose's toObject() method using Javascript's array method, map:book.chapters = ( await Chapter.find({ bookId: book._id }, 'title slug').sort({ order: 1 }) ).map((chapter) => chapter.toObject());Now, the updated definition for
Book.getBySlugis:static async getBySlug({ slug }) { const bookDoc = await this.findOne({ slug }); if (!bookDoc) { throw new Error('Book not found'); } const book = bookDoc.toObject(); book.chapters = ( await Chapter.find({ bookId: book._id }, 'title slug').sort({ order: 1 }) ).map((chapter) => chapter.toObject()); return book; }
You've reached the end of the Chapter 5 preview. To continue reading, you will need to purchase the book.