Professional Application Development in MEAN Stack - Part 9 - A
Date Published: 10/27/2018
This is an important part of series where we will create the Page Management APIs in node.js and front-end in Angular, we will be able to create the pages, add the content (text, images, links etc.) and attached them with the menu items that we have developed in earlier part as Menus Management. You would be able to create as many pages as you want and update them anytime without touching the application code, that will act as a simple Content Management System. For the page editor, we will use tinymce control for Angular that works great to provide us with different features to create an awesome front-end page. In fact, the article you are ready now and all others are written using Angular tinymce control.

Introduction

In this part, we will work on Page Managment APIs. In previous parts, you saw that we created the static pages for Services, Our Team  menu items etc. that is tedious if you want to update the content of any page because we need to rebuild and deploy the application every time. In this part, We are going to create the Page Management section where a user would be able to create the page content in the admin section, add the link, images, format the text, associate it with the menu item and publish it. We already have created the Menus Management section to create, read and update the menu items. This is very helpful because everytime when an end user would need to update the content, s/he can simply log in to the admin section and update the content of the page in Page Management section that would immediately be updated and viewed on the page, no need to rebuild and deploy the application, that would be our custom semi Content Managment system. 

Let's Start

It is strongly recommended to read all previous parts before moving to this one, can't promise you would understand all the steps I am going to list down without knowledge of previous parts.

      1. Please follow the Part 8 part and make sure application is working fine before starting this one. A fully functional Menu Managment section is pre-requisite for this part.
      2. As stated in the Introduction, we will create the Page Managment section, but first, we are going to create the Page Management RESTful in node.js using an Express.js framework. We will create page model class that would have page elements/fields and as soon we will access any API, the page collection would be automatically created in Mongo DB. (MongoDB has collection instead of Tables and each collection has Documents that are equivalent to rows/records in a relational database)
      3. In the server folder of MAZHARNCO solution, right click on models folder and select option New File, specify the file name pageMdl.js and hit enter. Paste the following code in the pageMdl.js file:  
        const mongoose = require('mongoose');
        
        const UploadSchema = mongoose.Schema({
          OrigName: {
            type: String
          },
          ActualName: {
            type: String
          },
          FileType:
          {
            type: String
          },
          Path: {
            type: String
          }
        });
        
        const PageSchema = mongoose.Schema({
          Title: {
            type: String,
            required: true
          },
          Content: {
            type: String,
            required: true
          },
          DatePublished: {
            type: Date,
            required: true
          },
          DateAdded: {
            type: Date,
            required: true
          },
          DateUpdated: {
            type: Date
          },
          Status: {
            type: String,
            required: true
          },
          MenuCode: {
            type: String,
            required: true
          },
          Images: [UploadSchema]
        });
        
        PageSchema.index({ '$**': 'text' });
        
        const Page = mongoose.model('Page', PageSchema);
        module.exports = Page;
      4. Let's try to understand it, you can see we have two schemas defined, one is UploadSchema that would be used to store any kind of attachment mostly for images and second is PageSchema that will store actual page information. One of the element/key Images in PageSchema is of UploadSchema type. So one page can have N number of images. MenuCode will hold the corresponding menu associated with a page whereas Content will store the entire page HTML, rest of elements are self-explanatory. The PageSchema.index({ '$**': 'text' }); will make full text search enabled that is helpful when we will implement the website-wide search functionality. 
      5. In previous step, you saw that we are also giving the images upload functionality, in order to facilitate it, we can use multer package that is node.js middleware and handle multipart/form-data which is primarily used for upload file. If you are an experienced developer you would have seen it many times when using HTML upload control. You would see it in action in the next step, first, let install it. Right click on server folder and select the option, Open in Terminal. In TERMINAL tab, enter the command: npm install -save multi
      6. We need two more packages for Page Management APIs, first is crypto, that we would use to create a random name for the image, second is mime-types to get the format and extension of the image to be uploaded. You will also see them in action in next steps that would give you a clear understanding, staying in Step 5 TERMINAL, after multer is successfully installed, enter these two commands (you can combine it in one like I am doing here): npm install -save crypto mime-types
      7. All the prerequisites for Page APIs are done, let's go ahead and create the page.js file and add the APIs code in it. Right click on routes folder and select the option New File, specify the file name page.js and add the following code in it: 
        const express = require('express');
        const path = require('path');
        const router = express.Router();
        const crypto = require('crypto');
        const mime = require('mime-types');
        var multer = require('multer');
        const fs = require('fs');
        const Page = require('../models/pageMdl');
        
        var storage = multer.diskStorage({
          destination: function (req, file, cb) {
            cb(null, './uploads/')
          },
          filename: function (req, file, cb) {
            crypto.pseudoRandomBytes(16, function (err, raw) {
              var d = new Date();
              cb(null, raw.toString('hex') + Date.now() + '.' + mime.extension(file.mimetype));
            });
          }
        });
        var upload = multer({ storage: storage }).single('photo');
        
        
        router.get('/page', function (req, res) {
          Page.find({}, function (err, page) {
            if (err) {
              res.json({ success: false, msg: err });
            } else {
              res.json({ success: true, data: page });
            }
          }).sort('-DatePublished');
        });
        
        
        router.get('/page/:id', function (req, res) {
          Page.findOne({ MenuCode: req.params.id }, function (err, page) {
            if (err) {
              res.json({ success: false, msg: err });
            } else {
              res.json({ success: true, data: page });
            }
          })
        });
        
        router.get('/page/search/:searchStr', function (req, res) {
          Page.find({
            $text: { $search: req.params.searchStr }
          }, function (err, page) {
            if (err) {
              res.json({ success: false, msg: err });
            } else {
              res.json({ success: true, data: page });
            }
          }).sort('-DatePublished');
        });
        
        router.post('/page', function (req, res) {
          let pageObj = new Page(req.body);
        
          pageObj.Images = req.body.Images == null ? [] : req.body.Images;
          pageObj.DateAdded = new Date();
          pageObj.DatePublished = new Date();
          pageObj.DateUpdated = new Date();
          pageObj.save(function (err) {
            if (err) {
              res.json({ success: false, msg: err });
              return;
            } else {
              res.json({ success: true, msg: "Successfully saved the page!", data: pageObj });
            }
          });
        });
        
        router.put('/page', function (req, res) {
          let pageObj = new Page(req.body);
          pageObj.DateUpdated = new Date();
        
          let query = { _id: req.body._id }
        
          Page.update(query, pageObj, function (err) {
            if (err) {
              res.json({ success: false, msg: err });
              return;
            } else {
              res.json({ success: true, msg: "Successfully updated the page!", data: pageObj.Images });
            }
          });
        });
        
        router.delete('/page/:id', function (req, res) {
        
          let query = { _id: req.params.id }
        
          Page.findById(req.params.id, function (err) {
            Page.remove(query, function (err) {
              if (err) {
                res.json({ success: false, msg: err });
                return;
              } else {
                res.json({ success: true, msg: "Successfully deleted the page!" });
              }
            });
          });
        });
        
        router.post('/page/upload', function (req, res, next) {
          var path = '';
          upload(req, res, function (err) {
            if (err) {
              res.json({ success: false, msg: err });
            }
            path = req.file.path;
            console.log(path);
            res.json({ success: true, data: req.file });
          });
        });
        
        ///api/article/img/id/aid
        router.delete('/page/img/:id/:iid/:fn', function (req, res) {
        
          fs.unlink(path.join(__dirname, '../uploads/' + req.params.fn), (err) => {
            if (err) {
              res.json({ success: false, msg: err });
              return;
            }
          });
        
          if (req.params.iid != 'x') {
            Page.findById(req.params.id, function (err, page) {
        
              page.Images.id(req.params.iid).remove();
        
              page.save(function (err) {
                if (err) {
                  res.json({ success: false, msg: err });
                  return;
                } else {
                  res.json({ success: true, msg: "Deleted" });
                }
              });
            });
          }
          else
            res.json({ success: true, msg: "Deleted" });
        });
        
        module.exports = router;
        
      8. The page.js has few more APIs than our standard GET, POST, PUT and DELETE APIs, we will not use all at once but at the end of this series you would see all APIs in action, Let understand the important part of the code:
        1. We installed multer in earlier steps, you can see in var storage = multer.diskStorage({.. code block, we are specifying the uploads folder as our destination file storage, and also crypto.pseudoRandomBytes will generate the 16 characters long file name to make it uniquely identifiable and avoid file replacement in case of the duplicate image name. mime.extension will return us proper file extension e.g. .jpeg, .gif that we are appending at the end of file name. So basically, this code block is only related to an uploaded image, it replaces the file name to some 16 characters long string and stores it in uploads folder at the same level where the server folder resides. You can also change the folder name and path if you want. The var upload = multer({ storage: storage }).single('photo'); will store a single file on a disk. Right click on server folder, select option New Folder and specify the name as uploads
        2. The router.get('/page', function (req, res) { ... is simple GET API to load all pages sorted by DatePublished.
        3. The router.get('/page/:id', function (req, res) { ... will get all pages (most likely one in our case) from a database by MenuCode. As explained earlier, the page should be associated with at least one menu item, so menu code is the required element for page document. 
        4. The router.get('/page/search/:searchStr', function (req, res) { ... would be used to search anything in page document that user will enter on front-end search textbox, you can see on a top right corner we have a textbox with Search button next to it that is not functional yet but in upcoming steps, we will implement that search functionality and use this API as a backend.
        5. The router.post('/page', function (req, res) { ... would be used to create the new page. You can see the pageObj.Images object is storing an empty array in case of no uploaded images. Remember that Images is of UploadsSchema type as we saw in pageMdl.js
        6. The router.put('/page', function (req, res) { ... is used to update the page, no magic code in there, pretty simple.
        7. The router.delete('/page/:id', function (req, res) { ... would be used to delete the page.
        8. The router.post('/page/upload', function (req, res, next) { ... would be used to upload the image, basically, save it in the uploads folder. You can see we are calling upload method that we created in step 1 of this section. On the front end, when a user would upload an image, this API would be called to store it in uploads folder and rest of image information e.g. image actual name, file type, and a path (that would return by this API) would be stored in session as Images array. Once user would add or update the page, the Images object would be passed to POST/PUT API (step 5) and stored it in the database (Images is of UploadsSchema type as we saw in pageMdl.js). This will be more clear when we will develop our front end section in Angular.
        9. The router.delete('/page/img/:id/:iid/:fn', function (req, res) { ... has three input parameters and would be used to delete the uploaded image, the id is page id to find the page from the database, the iid is image id and fn is the file name. The fs.unlick will delete the actual file from uploads folder and in rest of the code, we are first finding the page, then the image in the page and delete it. Remember, one page can have many images. 
        10. These APIs will make more sense once we would use them in frontend Angular application.
      9. That's pretty much on page model and APIs, the last steps are to add the APIs reference in mazharnco.js that is main entry file for a server application. Edit the mazharnco.js in server folder and replace its content with the following: 
        const express = require('express');
        const path = require('path');
        const bodyParser = require('body-parser');
        const cors = require('cors');
        const contacts = require('./routes/contact');
        const menus = require('./routes/menu');
        //Page APIs 
        const pages = require('./routes/page');
        const mongoose = require('mongoose');
        const config = require('./config/database');
        
        mongoose.connect(config.database, function (err) {
            if (err) {
                console.log('Not connected to the database: ' + err);
            } else {
                console.log('Successfully connected to MongoDB');
            }
        });
        
        var app = express();
        
        app.engine('html', require('ejs').renderFile);
        app.use(express.static(path.join(__dirname, './views')));
        app.use('/uploads', express.static(path.join(__dirname, '/uploads')));
        
        app.use(bodyParser.urlencoded({ extended: false }));
        app.use(bodyParser.json());
        app.use(cors());
        app.use("/api", contacts);
        app.use("/api", menus);
        //Page APIs
        app.use("/api",pages);
        
        app.get('*', function (req, res) {
            res.render(path.join(__dirname, './views/index.html')); // load our public/index.html file
        });
        
        const port =  process.env.PORT;
        
        app.listen(port, function () {
            console.log('Server started on port ' + port);
        });
      10. One extra line of code other than standard code is app.use('/uploads', express.static(path.join(__dirname, '/uploads'))); that will allow our application to use the uploads folder to upload the images and access them from front end when we will render the page. 
      11. Your page APIs should be working now, let's test them in Postman, first run the application, right click on server folder and select the option Open in Terminal, run the commandpm2 start environment.json --env development. Make sure it is green, you can also see the application status by the commandpm2 show mazharnco. This will show the brief description of running application along logs path that is good for seeing the errors. 
      12. Open the Postman is open, select the HTTP verb as Post and URL as http://localhost:3000/api/page , in the body, add the following JSON{"Title":"MMC Client","Content":"<p>This is a test page.</p>","Status":"Published","MenuCode":"client","DatePublished":null,"DateAdded":null,"DateUpdated":null,"Images":null}, hit the Send button, you should see the successful response. Make sure in the Headers tab, there is a key Content-Type is present and checked with the value: application/json.
      13. Open the Robo 3T and browse the mazharncoweb collection, expand the collection and you would see the new pages collection in it, double-click on it and you should be seeing a new document that you just created.
      14. Let's create a client application in Part 9 - B, since this is becoming a quite long article and I don't want to scare you with all this crap. Following is a Postman screenshot in case you want to compare with yours. 




Keywords: angular reactive form tutorial, Node.js nodeemailer, MEAN stack tutorial, Angular 5 tutorial for beginners, MEAN Stack tutorial for beginners, rxjs tutorial or beginner, rxjs vs Promise,nodemailer,pm2, node.js MongoDB, node.js mongoose configuration, angular data grid control, free angular data grid control, tinymce angular, angular text editor, angular dynamic page, multer, mime-types, save files on storage in node.js