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.list
static method finds allBook
MongoDB documents and returns an object with abooks
property, which is an array of correspondingbook
objects. We can use this method in the future to show all books to an Admin user at theAdmin
page - more on this in Chapter 6. 2.Book.getBySlug
static method finds one uniqueBook
MongoDB document by itsslug
, then returns the correspondingbook
object. We'll use it to display a single book, for example, when a Public or Customer user loads theReadChapter
page ( more on this later in this chapter). 3.Book.add
creates a newBook
MongoDB document. We'll call it, indirectly, from theAddBook
page in Chapter 6. This method returns a correspondingbook
object. 4.Book.edit
finds aBook
MongoDB document and updates some of its fields. This method is called by an Admin user indirectly from theEditBookPage
page in Chapter 6. This method returns a correspondingbook
object.
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.list
method (static async list(...)
) takes two arguments:offset
andlimit
. The method waits (await
) until all books are found with thefind
Mongoose API method and returns an array of book objects as thebooks
property of the object:return { books }
Inside the
Book.list
method, 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 = 0
ensures that we do not skip any books..limit(limit)
andlimit=10
returns 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 theskip
method is zero, so we don't need to specify it explicitly. However, let's keep theoffset
argument explicit so there is an easy way to modify its value in the future if needed. For example, we can modify the value foroffset
if we decide to add pagination to our list of books.
We discussed theasync/await
construct in detail in Chapter 3. We useasync/await
for practically all static methods of all data models.The static and async
Book.getBySlug
method (static async getBySlug(...)
) takes one argument:slug
. The main method waits (await
) until Mongoose's API methodfindOne
finds one book (slug
is unique, meaning allBook
MongoDB documents must have a unique value for theslug
field):slug: { type: String, required: true, unique: true, },
It's always a good habit to check for edge cases. What if a
Book
MongoDB 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.getBySlug
method: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
ReadChapter
page, we want to display a chapter's content. We need thebook.chapters
property to be populated with an array of a book's chapters -chapter
objects that correspond toChapter
MongoDB documents. To do so, we can call theChapter.find
Mongoose API method that returns an array of objects and usesbook._id
as 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.getBySlug
is: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.