Modern JavaScript Applications
上QQ阅读APP看书,第一时间看更新

Creating the services

Let's first build the services before building the image storage server and monolithic core.

We will build the database service first, as it only depends on the MongoDB server, which is already running. The upload service and monolithic core depend on it, therefore it needs to be built before these.

Database service

The database service will provide actions to add coupons, list verified coupons, list unverified coupons, verify a coupon, and delete a coupon. These actions will be used by the upload service and monolithic core.

Open the Initial/database-service directory. Inside the directory, you will find a package.json file and an app.js file. The app.js file is where you will write the code, and package.json lists the dependencies for the database service. The database service is dependent on the seneca and seneca-mongo-store plugins. Run the npm install command inside Initial/database-service to install the dependencies locally.

Here is the code to import the seneca module, create the seneca instance, attach the seneca-mongo-store plugin, and initialize the plugin to connect to MongoDB:

var seneca = require("seneca")();

seneca.use("mongo-store", {
  name: "gocoupons",
  host: "127.0.0.1",
  port: 27017
});

Here we are using gocoupons as the database name. I am assuming that the MongoDB server is running locally on the default port 27017.

The following is the code to create an action that allows you to add a coupon:

seneca.add({role: "coupons-store", cmd: "add"}, function(args, respond){
  var coupons = seneca.make$("coupons");
  var data = coupons.data$({title: args.title, desc: args.desc, email: args.email, url: args.url, price: args.price, discount: args.discount, thumbnail_id: args.thumbnail_id, verified: false});
  data.save$(function(err, entity){
    if(err) return respond(err);

    respond(null, {value: true});
  });
});

We will store the coupons in a collection named coupons. Here we are setting the verified property of the document to false, that is, whenever a new coupon is submitted by a user, we will make it unverified so that the administrator can retrieve this newly submitted coupon and verify it manually.

The thumbnail_id property doesn't hold the complete URL of the coupon thumbnail, instead it's just the filename.

Here is the code to create an action to retrieve the verified coupons:

seneca.add({role: "coupons-store", cmd: "list"}, function(args, respond){
  var coupons = seneca.make$("coupons");
  coupons.list$({verified: true, limit$:21, skip$: args.skip}, function (err, entity){
    if(err) return respond(err);

    respond(null, entity);
  })
});

This action retrieves maximum 21 coupons and it takes a skip argument that is used to skip some documents, making it possible to implement pagination using this action.

The following is the code to create an action to retrieve the unverified coupons:

seneca.add({role: "coupons-store", cmd: "admin_list"}, function(args, respond){
  var coupons = seneca.make$("coupons");
  coupons.list$({verified: false}, function (err, entity){
    if(err) return respond(err);

    respond(null, entity);
  })
});

This action will be used to retrieve coupons to display on the admin panel for the administrator to accept or reject a coupon.

Here is the code to create an action to verify a coupon, that is, change the verified property from false to true:

seneca.add({role: "coupons-store", cmd: "verified"}, function(args, respond){
  var coupons = seneca.make$("coupons");
  var data = coupons.data$({id: args.id, verified: true});
  data.save$(function(err, entity){
  if(err) return respond(error);

    respond(null, {value: true});
  });
});

This action will be invoked when the admin accepts a coupon to be displayed publicly.

Here is the code to create an action to delete a coupon:

seneca.add({role: "coupons-store", cmd: "delete"}, function(args, respond){
  var coupons = seneca.make$("coupons");
  coupons.remove$({id: args.id});
  respond(null, {value: true}); 
});

This action will be invoked when the admin rejects a coupon.

Now that we have created all the actions for our database service, let's expose these actions via the network so that the other servers can call them. Here is the code to do this:

seneca.listen({port: "5010", pin: {role: "coupons-store"}});

Now go ahead and run the database service using the node app.js command.

URL config service

The upload services use the URL config service to find the base URL of the monolithic core so that it can redirect the user there once the coupon is submitted successfully. Also, the monolithic core uses this service to find the base URL of the image storage server and upload service so that it can include them in the HTML code.

Open the Initial/config-service directory. Inside the directory, you will find a package.json file and an app.js file. The app.js file is where you will write the code and package.json lists the dependencies for the config service. URL config service is only dependent on seneca. Run the npm install command inside Initial/config-service to install the dependencies locally.

The following is the code to import the seneca module and create actions to return the base URLs of the upload service, monolithic core, and image storage server:

var seneca = require("seneca")();

seneca.add({role: "url-config", cmd: "upload-service"}, function(args, respond){
  respond(null, {value: "http://localhost:9090"});
});

seneca.add({role: "url-config", cmd: "monolithic-core"}, function(args, respond){
  respond(null, {value: "http://localhost:8080"});
});

seneca.add({role: "url-config", cmd: "image-storage-service"}, function(args, respond){
  respond(null, {value: "http://localhost:7070"});
});

seneca.listen({port: "5020", pin: {role: "url-config"}});

Now go ahead and run the URL config service using the node app.js command.

Upload service

The upload service handles the new coupon form submission. The form consists of a coupon title, URL, description, price, discount price, and a thumbnail. The content type of form submission is multipart/form-data, as it is uploading an image file.

Open the Initial/upload-service directory. Inside the directory, you will find a package.json file and an app.js file. The app.js file is where you will write the code and package.json lists the dependencies for the upload service. The upload service is dependent on seneca, express, connect-multiparty, path, fs and request packages. Run the npm install command inside Initial/upload-service to install the dependencies locally.

The following is the code to import the modules:

var seneca = require("seneca")();
var app = require("express")();
var multipart = require("connect-multiparty")();
var path = require("path");
var fs = require("fs");
var request = require("request");

There are chances that the users may upload images with the same name. We don't want images with the same name to overwrite each other. Therefore, we need rename every image with a unique name. The following is the code for defining a function to generate a unique number, which will be used as an image name:

function uniqueNumber() {
  var date = Date.now();

  if (date <= uniqueNumber.previous) {
    date = ++uniqueNumber.previous;
  } else {
    uniqueNumber.previous = date;
  }

  return date;
}

uniqueNumber.previous = 0;

function ID(){
  return uniqueNumber();
};

Now, for the upload service to be able to communicate with the database and URL config services, we need to add them to the upload service seneca instance. The following is the code to do this:

seneca.client({port: "5020", pin: {role: "url-config"}});
seneca.client({port: "5010", pin: {role: "coupons-store"}});

Now we need to define an express route to handle POST requests submitted to the /submit path. Inside the route handler, we will rename the image, upload the image to image storage server, add the metadata of the coupon to MongoDB using the database service, and redirect to the monolithic core with the status stating that the form was submitted successfully. Here is the code to define the route:

//declare route and add callbacks
app.post('/submit', multipart, function(httpRequest, httpResponse, next){

  var tmp_path = httpRequest.files.thumbnail.path;
  var thumbnail_extension = path.extname(tmp_path);
  var thumbnail_directory = path.dirname(tmp_path);
  var thumbnail_id = ID();
  var renamed_path = thumbnail_directory + '/' + ID() + thumbnail_extension;

  //rename file
  fs.rename(tmp_path, renamed_path, function(err) {
    if(err) return httpResponse.status(500).send("An error occured");

    //upload file to image storage server
    seneca.act({role: "url-config", cmd: "image-storage-service"}, function(err, storage_server_url){
      var req = request.post(storage_server_url.value + "/store", function (err, resp, body){
        fs.unlink(renamed_path);

        if(err) return httpResponse.status(500).send("An error occured");

        if(body == "Done")
        {
          //store the coupon
          seneca.act({role: "coupons-store", cmd: "add", title: httpRequest.body.title, email: httpRequest.body.email, url: httpRequest.body.url, desc: httpRequest.body.desc, price: httpRequest.body.price, discount: httpRequest.body.price, thumbnail_id: thumbnail_id + thumbnail_extension}, function(err, response){
            if(err)
            {
              //delete the stored image
              request.get(storage_server_url + "/delete/" + thumbnail_id + thumbnail_extension);
              httpResponse.status(500).send("An error occured");
              return;
            }
            seneca.act({role: "url-config", cmd: "monolithic-core"}, function(err, response){
              if(err) return httpResponse.status(500).send("An error occured");

              //redirect to monolithic core
              httpResponse.redirect(response.value + "/?status=submitted");  
            });
          });
        }
      });

      var form = req.form();
      form.append("thumbnail", fs.createReadStream(renamed_path));
      form.append("name", thumbnail_id + thumbnail_extension);
    });
  });
});

Here is how the preceding code works:

  • First we added a callback provided by the connect-multiparty module, which parses the multipart/form-data body and moves the files to a temporary location.
  • In the second callback, we performed our custom operations. In the second callback, we first renamed the file so that every image file gets a unique name. Renaming is done using the rename method of the filesystem module.
  • Then we uploaded the image file to the image storage server using the post method of the request module.
  • After this, we deleted the local version of the image file using the unlink method of the filesystem module.
  • If uploading the image to the image storage server failed for some reason, then we will return an HTTP internal server error to the client.
  • If the image got uploaded to the image storage server successfully, then we will add the coupon metadata to MongoDB via the database service.
  • If, for some reason, the metadata did not get added, we will delete the previously stored image in the image storage server and then return an HTTP internal server error to the client.
  • If the coupon metadata got added successfully, we will retrieve the base URL of monolithic core from the URL config service and redirect there with a /?status=submitted query string, which indicates that the form was submitted successfully. When the monolithic core sees this query string, it displays a message saying that the coupon was submitted successfully.
  • In case the URL config service didn't respond for some reason, we will return an HTTP internal server error to the client.

So what you need to keep in mind while coding such services is that you need to handle all sorts of failures and also roll back changes if a failure occurs. Now, this also makes it easy to update and redeploy the database service, URL config service, and image storage server as the upload service handles the failure of these services and provides a feedback to the user.

Now we have defined our routes. Finally, we need to start the Express server. The following is the code to do so:

app.listen(9090);

Now go ahead and run the upload service using the node app.js command.