22.3. Architecture - Express and Mongo

22.3.1. The Repo

The code presented in this section is available as a repo for cloning from https://gitlab.com/arosano/employeeProject.git.

In this section we shall look at another case. This time adding models to make the database handling easier. We shall upgrade our MongoDB handling by introducing and using the module mongoose over mongodb the we have used so far. The following example will showcase inserting an employee, and a user document into the Mongo database via their models. Mongo models are implemented via mongoose schemas, reminiscent of JavaScript classes.

Before discussing the content, we prepare, as usual, with

$ npx express --view=pug --git employeeProject
...
$ cd employeeProject
$ mkdir models
4 mkdir controllers
$ npm install
$ git init
$ git add .
$ git commit -m 'initial commit præ festum'

22.3.2. Part Zero, the Database Implementation

The database intends to demonstrate the use of mongoose in a 1:N scenario with total participation with the additional purpose of demonstrating joins in the non relational MongoDB.

Figure 22.1. The Database ER Diagram

Example 22.1. The Department Schema, employeeProject/models/Department.js
const mongoose = require("mongoose");

const schema = mongoose.Schema({
    name: {
        type: String,
        required: true,
        unique: true
    },
    address: {
        street: {
            type: String,
            required: true
        },
        no: {
            type: Number,
            required: true
        },
        place: String,
        zip: {
            type: Number,
            required: true
        },
        town: {
            type: String,
            required: true
        }
    }
});

module.exports = mongoose.model("Department", schema, 'department');

Example 22.2. The Program Schema, employeeProject/models/Program.js
const mongoose = require("mongoose");
const autopopulate = require('mongoose-autopopulate');

const schema = mongoose.Schema({
    name: {
        type: String,
        required: true,
        unique: true
    },
    programIdent: {
        type: Number,
        required: true,
        unique: true
    },
    duration: {
        type: Number,
        required: true
    },
    department: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'Department',
        required: true,
        autopopulate: true
    }
});

schema.plugin(autopopulate);
module.exports = mongoose.model("Program", schema, 'program');

Example 22.3. The Person Schema, employeeProject/models/Person.js
const mongoose = require("mongoose");
const autopopulate = require('mongoose-autopopulate');

const schema = mongoose.Schema({
    cpr: {
        type: String,
        validate: {
            validator: function(v) {
                return /\d{6}-\d{4}/.test(v);
            },
            message: props => `${props.value} is not a valid CPR number!`
        },
        unique: true,
        required: true
    },
    email: {
        type: String,
        validate: {
            validator: function(v) {
                return /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,9}/.test(v);
            },
            message: props => `${props.value} is not a valid email address!`
        },
        unique: true,
        required: true
    },
    firstname: {
        type: String,
        required: true
    },
    middlename: {
        type: String,
    },
    lastname: {
        type: String,
        required: true
    },
    password: {
        type: String,
        required: true
    },
    role: {
        type: String,
        enum: ['admin', 'other', 'pending'],
        default: 'pending'
    },
    program: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'Program',
        required: true,
        autopopulate: true
    }
});

schema.plugin(autopopulate);
module.exports = mongoose.model("Person", schema, 'person');

Example 22.4. The Student Schema, employeeProject/models/Student.js
const mongoose = require("mongoose");
const autopopulate = require('mongoose-autopopulate');

const schema = mongoose.Schema({
    cpr: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'Person',
        required: true,
        autopopulate: true
    },
    inducted: {
        type: Date,
        required: true
    },
    profile: {
        type: String,
        enum: ['tap', 'student', 'faculty'],
        default: 'student'
    }
});

schema.plugin(autopopulate);
module.exports = mongoose.model("Student", schema, 'student');

22.3.3. Part One - Non People Objects

First install the mongoose module by issuing

npm i mongoose

on the CLI in the project directory. Then let us look at our exemplary code.

Example 22.5. employeeProject/routes/index.js
var express = require('express');
var router = express.Router();
const TITLE = 'Academy Pattern Project';
const con = require('../controllers/controller');

/* GET home page. */
router.get('/', function(req, res, next) {
  res.render('index', {
        title: TITLE,
        subtitle: 'Front Page'
    });
});

/* GET show departments */
router.get('/departments', async function(req, res, next) {
    let departments = await con.getDepts({}, {sort: {title: 1}});   // read depts from db
    res.render('showdepts', {
        title: TITLE,
        subtitle: 'Display Departments',
        departments
    });
});
/* GET show html form for departments */
router.get('/deptform', function(req, res, next) {
    res.render('deptformv', {
        title: TITLE,
        subtitle: 'Department Entry Form'
    });
});
/* POST handle form data for departments */
router.post('/deptform', function(req, res, next) {
    con.postDept(req, res, next);                                   // write department into db
    res.redirect('/');
});

/* GET show programs */
router.get('/programs', async function(req, res, next) {
    let programs = await con.getPrograms({}, {sort: {title: 1}});   // read programs from db
    res.render('programs', {
        title: TITLE,
        subtitle: 'Display Programs',
        programs
    });
});
/* GET show html form for programs */
router.get('/program', async function(req, res, next) {
    let departments = await con.getDepts({});                       // read departments, programs are in them
    res.render('program', {
        title: TITLE,
        subtitle: 'Department Entry Form',
        departments
    });
});
/* POST handle form data for programs */
router.post('/program', function(req, res, next) {
    con.postProg(req, res, next);                                   // write program into db
    res.redirect('/');
});

module.exports = router;

Example 22.6. employeeProject/controllers/controller.js
const mongoose = require("mongoose");
const Department = require("../models/Department");
const Program = require("../models/Program");

module.exports = {
    getDepts: async function (que, sort) {
        const depts = await Department.find(que, null, sort);                     // read
        return depts;
    },

    postDept: async function (req) {
        let address = {
            street: req.body.street,
            no: req.body.no,
            place: req.body.place,
            zip: req.body.zip,
            town: req.body.town
        };

        let dept = new Department({                                               // create object in schema-format
            name: req.body.name,
            address: address
        });

        try {
            let rc = await Department.create(dept);
            return rc;
        } catch (err) {
            console.log(err)
        }
    },

    getPrograms: async function (que, sort) {
        const progs = await Program.find(que, null, sort);                        // read
        return progs;
    },

    postProg: async function (req) {
        let program = new Program ({
            name: req.body.name,
            programIdent: req.body.minid,
            duration: req.body.duration,
            department: req.body.department
        });

        try {
            let rc = await Program.create(program);
            return rc;
        } catch (err) {
            console.log(err)
        }
    }
}

22.3.4. Part Two - People

In this part we shall use the designated user router. Express introduces that for separation of concerns.

Example 22.7. employeeProject/routes/users.js
var express = require('express');
var router = express.Router();
const TITLE = 'Academy Pattern Project';
const con = require('../controllers/userController');
const dep = require('../controllers/controller');

/* GET show people */
router.get('/show', async function(req, res, next) {
    let people = await con.getPeople({}, {sort: {title: 1}});   // read people from db
    res.render('people', {
        title: TITLE,
        subtitle: 'Display People',
        people
    });
});
/* GET show html form for people */
router.get('/register', async function(req, res, next) {
    let programs = await dep.getPrograms({});               // read programs, people work in them
    res.render('register', {
        title: TITLE,
        subtitle: 'Register People',
        programs
    });
});
/* POST handle form data for people */
router.post('/register', con.postPerson, con.postStudent, async function(req, res, next) {
    let programs = await dep.getPrograms({});               // read programs, people work in them
    res.render('register', {
        title: TITLE,
        subtitle: 'Register People',
        programs
    });
});

module.exports = router;

Example 22.8. employeeProject/controllers/userController.js
const mongoose = require("mongoose");
const bcrypt = require("bcryptjs");
const Person = require("../models/Person");
const Student = require("../models/Student");

module.exports = {
    getPeople: async function (que, sort) {
        const people = await Student.find(que, null, sort);   // read
        console.log(JSON.stringify(people, null, 4));
        return people;
    },

    getPerson: async function (que, sort) {
        const departments = await Dept.find(que, null, sort); // read
        return departments;
    },

    postPerson: async function (req, res, next) {
        let hash = await bcrypt.hash(req.body.password, 10);
        try {
            let person = new Person({                             // create object in schema-format
                cpr: req.body.cpr,
                email: req.body.email,
                firstname: req.body.firstname,
                middlename: req.body.middlename,
                lastname: req.body.lastname,
                password: hash,
                program: req.body.program
            });
            await Person.create(person);
            res.locals.id = person._id;
            next();
        } catch (err) {
            let message;
            if (err.errors.cpr)
                message = err.errors.cpr.properties.message;
            else (err.errors.email)
                message = err.errors.email.properties.message;
                res.render('register', {
                    title: TITLE,
                    subtitle: 'Register People',
                    programs
                });
            next(err);
        }
    },

    getStudent: async function (que, sort) {
        const students = await Dept.find(que, null, sort);  // read
        return students;
    },

    postStudent: async function (req, res, next) {
        if (req.body.type !== 'student') {
            next();
        }
        let student = new Student({                         // create object in schema-format
            cpr: res.locals.id,
            inducted: req.body.inducted
        });
        try {
            await Student.create(student);
            next();
        } catch (err) {
            console.log(err);
            next(err);
        }
    }
};

22.3.5. Hommage a DRY

All the individual database activities require a connection to a database server, hence, written once, as a a singleton:

Example 22.9. employeeProject/models/MongoHandler.js
/*
 * MongoConnect as a Singleton
 */
const mongoose = require('mongoose');
const MongoParams = require('../config/MongoParams').MongoParams;
const PARAMS =  {
    useNewUrlParser: true,
    useUnifiedTopology: true
};

class MongoConnection {
    static conn = false;

    static async mongoConnect() {
        if (!MongoConnection.conn) {
            try {
                await mongoose.connect(MongoParams.CONSTR, PARAMS);
                MongoConnection.conn = mongoose.connection;
                console.log(`Connected to mongo server`);
            } catch (err) {
                console.log('something mongo???' + err);
            }
        }
        return MongoConnection.conn
    }
}
module.exports = MongoConnection;

with these params

Example 22.10. employeeProject/config/MongoParams.js
const DBS = process.env.DBS;
const DBN = process.env.DBN;

module.exports = {
    MongoParams: {
        CONSTR: `mongodb://${DBS}:27017/${DBN}`
    }
};

22.3.6. Part Three - Views

In this part we shall use the designated user router. Express introduces that for separation of concerns. This is a fragment.

Example 22.11. The Common Page Top, employeeProject/views/layout.pug
doctype html
html
    head
        meta(charset='utf-8')
        title= title
        link(rel='stylesheet', href='/stylesheets/style.css')
        script(src='/javascripts/page.js')
    body
        header
            nav
                h1= title
                ul
                    li
                        a(href='/') Home
                    li
                        a(href='/departments') Departments
                    li
                        a(href='/deptform') Reg Departments
                    li
                        a(href='/programs') Programs
                    li
                        a(href='/program') Reg Programs
                    li
                        a(href='/users/show') People
                    li
                        a(href='/users/register') Reg People
        block content

Example 22.12. Show People, employeeProject/views/people.pug
extends layout

block content
    main.otherpage
        aside
        section
            h2= subtitle
            table.disp
                tr
                    th Email
                    th Name
                    th Role
                    th Program
                    th Department
                each pers in people
                    tr
                        td #{pers.cpr.email}
                        td #{pers.cpr.firstname} #{pers.cpr.middlename} #{pers.cpr.lastname}
                        td #{pers.cpr.role}
                        td #{pers.cpr.program.name}
                        td #{pers.cpr.program.department.name}
        aside
    include footer.pug

Example 22.13. People Form, employeeProject/views/register.pug
extends layout

block content
    main.otherpage
        aside
        section
            h2= subtitle
            form(id='registerForm' action='/users/register' method='post')
                table
                    tr
                        td.col_l CPR
                        td
                            input(type='text' name='cpr' placeholder='ddmmyyyy-ssss' minlength='11' required)
                    tr
                        td.col_l Email
                        td
                            input(type='email' name='email' placeholder='a@b.cc' required)
                    tr
                        td.col_l User, First Name
                        td
                            input(type='text' name='firstname' placeholder='firstname' required)
                    tr
                        td.col_l User, Middle Name
                        td
                            input(type='text' name='middlename' placeholder='middlename')
                    tr
                        td.col_l User, Last Name
                        td
                            input(type='text' name='lastname' placeholder='lastname' required)
                    tr
                        td.col_l Induction Date
                        td
                            input(type='Date' name='inducted' required)
                    tr
                        td.col_l Password
                        td
                            input(type='password' id='password' name='password' placeholder='password, min 16 chars' minlength='16' required)
                    tr
                        td.col_l Password Again
                        td
                            input(type='password' name='password1' placeholder='password, must match the above' minlength='16' required)
                    tr
                        td.col_l Program
                        td
                            select(name='program' required)
                                each prog in programs
                                    option(value=prog._id) #{prog.name}
                    tr
                        td.col_l Person Type
                        td
                            select(name='type' required)
                                each thing in ['student']
                                    option #{thing}
                    tr
                        td
                        td
                            input(type='submit' value='Go')
        aside
    include footer.pug

Example 22.14. Client Side Validation, employeeProject/public/javascripts/page.js
'use strict';
const $ = function (nml) { return document.getElementById(nml); };

const validate = function (e) {         // validation example, if not alike, prevent submission
    if ($('registerForm').password.value !== $('registerForm').password1.value) {
        e.preventDefault();
        window.alert('Two entered passwords do not match');
        console.log($('registerForm').password+':'+$('registerForm').password1);
        $('password').select();
        return false;
    }
};

const init = function () {
    if ($('registerForm')) {            // looking for particular form, if found setup validation
        $('registerForm').addEventListener('submit', validate);
    }
};
window.addEventListener('load', init);