Edward's Tech Site

this site made with Next.js 13, see the code

HOWTO: Nov 25, 2021 - Mongoose
Create Node/Express/EJS/Mongo site with Mongoose subdocument relationships
  • these instructions show you how to create a book store that displays books, the information of which comes from two collections in the MongoDB database
    • the Mongoose ODM (Object Document Mapper) is used to connect (with ref and populate()) the two schemas for the collections Books and Persons, much as JOIN does in an SQL database between tables
    • the collection relationships are such:
    • this site also uses the MVC pattern (Model-View-Controller) to organize how data is retrieved, e.g. a Controller queries Models for data, and then passes this data to the (EJS) Views
    • the site only consists of one page (/src/app.js)
    • if anything is unclear, see the code of the finished site
  • the finished site will look like this:
  • set up basics
    • git init
      • .gitignore
    • npm init -y
      • "type": "module",
    • src/app.js
    • "start": "npx nodemon src/app.js"
  • install
    • npm i express ejs mongoose
  • import
    • import express from 'express';
      import path from 'path';
  • Express/EJS plumbing
    • const app = express();
      const __dirname = path.resolve(path.dirname(''));
      const port = 3047;
      const staticDirectory = path.join(__dirname, './public');
       
      app.set('view engine', 'ejs');
      app.set('views', path.join(__dirname, './src/views'));
      app.use(express.static(staticDirectory));
  • listen to port
    • app.listen(port, () => {
      console.log(`Now listening on port http://localhost:${port}`);
      });
  • test in terminal
    • npm start
  • add endpoint
    • app.get('/', async (req, res) => {
      res.send('testing');
      });
  • test in browser
    • npm start
  • create EJS page: src/views/index.ejs
    • <html lang="en">
      <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible"
      content="IE=edge">
      <meta name="viewport"
      content="width=device-width, initial-scale=1.0">
      <title>Tech Bookstore</title>
      </head>
      <body>
      <h1>Tech Bookstore</h1>
      </body>
      </html>
  • update endpoint to serve the EJS page
    • res.render('index', {
      pageTitle: "Tech Bookstore"
      });
  • test in browser
  • set up Mongoose MVC
    • make a dev folder and create these JSON files in it
    • create database, collections and import data
      • mongoimport --db bookstore --collection books --type json --file mongo.books.json --jsonArray
      • mongoimport --db bookstore --collection persons --type json --file mongo.persons.json --jsonArray
    • connect to database in app.js:
      • import mongoose from 'mongoose';
      • mongoose.connect('mongodb://localhost:27017/bookstore');
  • create models
    • src/models/persons.js
      • import mongoose from 'mongoose';
         
        const Schema = mongoose.Schema;
         
        const personsSchema = mongoose.Schema({
        firstName: String,
        lastName: String,
        email: String
        }, { collection: "persons" });
        const PersonsModel = mongoose.model("Person", personsSchema);
         
        export default PersonsModel;
    • src/models/books.js
      • import mongoose from 'mongoose';
        import PersonsModel from '../models/persons.js';
         
        const Schema = mongoose.Schema;
         
        const booksSchema = mongoose.Schema({
        title: String,
        author: { type: Schema.ObjectId, ref: 'Person' },
        url: String,
        customers: [{ type: Schema.ObjectId, ref: 'Person' }],
        isbn: String
        }, { collection: "books" });
        const BooksModel = mongoose.model("Book", booksSchema);
         
        export default BooksModel;
  • create controller
    • note there is only one: books
    • src/controllers/books.js
      • import BooksModel from '../models/books.js';
         
        export const getAllBooks = async () => {
        const books = await BooksModel.find({})
        .populate("author")
        .populate("customers");
        return books;
        }
  • load data in endroute and send to view
    • import BooksController
      • import * as BooksController from './controllers/books.js';
    • update endroute
      • const books = await BooksController.getAllBooks();
        res.render('index', {
        pageTitle: "Tech Bookstore",
        books
        });
  • test if books are in view
    • <p>there are <%=books.length%> books</p>
    • you should see this:
    • note that in the books model file, you must import PersonsModel even though VSCode indicates that it's not used
      • test this by commenting the import line out and getting an erro
      • uncomment the line and it will work again
      • the persons model is being used by the booksSchema in ref: 'Person'
  • add the EJS code to display the books to index.ejs
    • <% books.forEach(book=> { %>
      <div class="book">
      <a href="<%=book.url%>" target="_blank"> <img src="images/<%=book.isbn%>.jpg"
      alt=""></a>
      <div class="info">
      <div class="title">
      <%=book.title%>
      </div>
      <div class="author">by <span class="main">
      <%=book.author.firstName%>
      <%=book.author.lastName%>
      </span></div>
      <ul class="customers">
      <div class="header">People who have bought this book:</div>
      <% book.customers.forEach(customer=> { %>
      <li>
      <%=customer.firstName%>
      <%=customer.lastName%>
      <span class="email">(<%=customer.email%>)</span>
      </li>
      <% })%>
      </ul>
      </div>
      </div>
      <% });
      %>
  • test in browser
    • you should have raw HTML of data from your database:
  • add the images
  • test again in browser
    • you should see the graphics in your raw HTML:
  • add styling
    • create public/css/main.scss
      • body {
        background-color: rgb(37, 3, 3);
        color: rgb(185, 183, 153);
        font-family: "Gill Sans", "Gill Sans MT", Calibri, "Trebuchet MS", sans-serif;
        font-size: 1.5rem;
        padding: 20px;
         
        div.book {
        background-color: rgb(71, 22, 22);
        margin: 0 0 20px 0;
        color: #ccc;
        padding: 20px;
        display: flex;
        img {
        width: 120px;
        box-shadow: 3px 3px 13px 3px #000000;
        }
        div.info {
        margin: 0 0 0 25px;
        }
        div.title {
        font-size: 1.8rem;
        }
        div.author {
        .main {
        font-size: 1.4rem;
        font-style: italic;
        color: rgb(235, 235, 154);
        }
        }
        ul.customers {
        color: #aaa;
        font-size: 0.9rem;
        li {
        margin: 0 0 0 -15px;
        font-style: italic;
        .email {
        color: #777;
        }
        }
        }
        div.header {
        margin: 0 0 4px -39px;
        }
        }
        }
    • in index.ejs add the link to the style sheet (to css/main.css, not css/main.scss!)
      • <link rel="stylesheet" target="_blank" href="css/main.css">
    • implement automatic sass-to-css transpiling
      • install the VSCode extension Live Sass Compiler
      • while on the extension intro page, click on "Watch Sass"
        • the text will then change to "Watching..."
  • test in browser, your site should look like this:
  • understand the relationship between the books and persons models:
    • in the database, find the person Josefine Yasper
    • change her name to Jasper
    • reload the page in the browser
    • note that both customers were changed to Josefine Jasper
  • Challenge:
    • add an author property on each comment
    • each comment field will contain the id of a person in the persons collection
    • next to each comment, display the name of the comment's author
    • solution will look like this:
    • here is one solution
    • it uses Mongoose deep population and in shown in the BooksController:
      • import BooksModel from '../models/books.js';
         
        export const getAllBooks = async () => {
        const books = await BooksModel.find({})
        .populate("author")
        .populate("customers")
        .populate([
        {
        path: "comments",
        model: "Comment",
        select: "author message datetime",
        populate: {
        path: "author",
        model: "Person"
        }
        }
        ]);
        return books;
        }
  • this site can be viewed running on Heroku