10

Deploying a Sequelize Application

After installing an admin dashboard, configuring our web application to book flights, and having built a backend server, we are now ready to start developing the frontend interface along with deploying the application. Just in time too, because our board members want to see some progress, and they would like to see a working prototype for purchasing a ticket.

Throughout this chapter, and to meet the requirements of our board members, we will need to do the following:

  • Refactor some of our current routes and add another route for listing flight schedules
  • Integrate Express’ static middleware and secure the admin interface
  • Create a page to list and book flights
  • Deploy the application to a service such as Fly.io

Technical requirements

For the tasks in this chapter, we will be installing the following additional software:

  • A version control manager called Git
  • The Fly.io CLI for deploying to a cloud application platform

You can find the code files for this chapter on GitHub at https://github.com/PacktPublishing/Supercharging-Node.js-Applications-with-Sequelize/tree/main/ch10.

Refactoring and adding flight schedule routes

Before we start creating the customer interface for purchasing a boarding ticket, we will need to make several adjustments to our code base. Let us begin by creating a new file located at routes/airplanes.js and moving the app.post('/airplanes', …) and app.get('/airplanes/:id', …) blocks into that file as follows:

async function getAirplane(req, res) {
    const airplane = await models.Airplane.findByPk
     (req.params.id);
    if (!airplane) {
        return res.sendStatus(404);
    }
    res.json(airplane);
}
exports.getAirplane = getAirplane;

This route will return an Airplane model record based on the primary key, which is defined in Express’ Request object (indicated by the :id symbol). If there were no records to be found, then we will return a 404 (not found) status.

Next, we will take the createAirplane code block from routes/flights.js and move it into the routes/airplanes.js file:

async function createAirplane(req, res) {
    const { name, seats } = req.body;
    try {
        const airplane = await models.Airplane.create({
            planeModel: name,
            totalSeats: seats,
        });
        return res.json(airplane);
    } catch (error) {
        res.status(500).send(error);
    }
}
exports.createAirplane = createAirplane;

Within routes/flights.js, we will want to add a new handler called flightSchedules:

async function flightSchedules(req, res) {
    const records = await models.FlightSchedule.findAll({
       include: [models.Airplane]
    });
    res.json(records);
}
exports.flightSchedules = flightSchedules;

After that, within the index.js file, in the project’s root directory, we can remove the app.get('/', …) block and modify the route requiring blocks (just above the block that we removed) to match the new method paths as follows:

const { bookTicket } = require("./routes/tickets")
const { createSchedule, flightSchedules } = 
require("./routes/flights");
const { getAirplane, createAirplane } = 
require("./routes/airplanes");

The app.get('/airplanes/:id', …) block should now look as follows:

app.get('/airplanes/:id', getAirplane);

And below that, we can add the flight schedule route:

app.get('/flights', flightSchedules);

Next, we will want to adjust the error returned from the customers model. Within models/customers.js, replace the existing attributes with the following code:

    name: {
      type: DataTypes.STRING,
      validate: {
        notEmpty: {
            msg: "A name is required for the customer",
        }
      }
    },
    email: {
      type: DataTypes.STRING,
      validate: {
        isEmail: {
            msg: "Invalid email format for the customer",
        }
      }
    }

The last modification for flights and booking a ticket involves making some adjustments to the routes/tickets.js file. First, we will want to add Sequelize’s ValidationError at the top of the file:

const { ValidationError } = require("@sequelize/core");

Since we will be finding, or creating, a customer throughout the booking process, we will want to change the req.body line to this:

const { scheduleId, seat, name, email } = req.body;

And below that line, we will add the following:

const [customer] = await models.Customer.findOrCreate({
    where: {
        email,
    },
    defaults: {
        name,
    }
});

This will tell Sequelize to find or create a customer record using the email as a key and will hydrate the record with the name (if the record is new) from the POST request.

Just above the await schedule.addBoardingTicket(…) block, we will want to add a method that defines the customer association for the newly created boarding ticket:

await boardingTicket.setCustomer(
 customer,
 { transaction: tx }
);

The remaining modification for this file is replacing the catch block with the following code:

    } catch (error) {
        if (error instanceof ValidationError) {
            let errObj = {};
            error.errors.map(err => {
               errObj[err.path] = err.message;
            });
            return res.status(400).json(errObj);
        }
        if (error instanceof Error) {
            return res.status(400).send(error.message);
        }
        return res.status(400).send(error.toString());
    }

This error block will check whether the incoming error is a Sequelize ValidationError type and if so, will map out the errors to errorObj with the column (err.path) as a key and the error message (err.message) as the value – then, it will return the error object. The next if block will check whether the error is a generic Error type, and if so, return the error.message value – otherwise, it will return the error variable as a string. This will provide a more ergonomic way of handling errors for a quick prototype website.

Those are all of the modifications that are necessary for managing flights and creating flight tickets. The next step is to set the foundation for our static assets and secure our admin dashboard.

Integrating Express’ static middleware and securing the admin interface

Before exposing our application to the general public, we will need to secure the admin dashboard routes, along with exposing the static assets for frontend development. First, we will want to create a new directory with an empty file located at public/index.html. After that, we can start making modifications to the index.js file (within the project’s root directory). At the top, we will need Node.js’ path module:

const path = require("path");

Just below the app.use('/graphql', server) block, we will want to tell Express to serve static assets that are found within the public directory:

app.use(express.static(path.join(__dirname, "public")));

Express will try to find a matching file with the associated route in the public directory before cascading down to our API routes (for example, /airplanes or /flights). The reason why we use path.join here is to avoid mismatches from relative paths, which allows us to run the application from any directory.

Next, we will want to secure our admin dashboard – in the name of brevity, we will use the HTTP authentication method. This will require us to install the express-basic-auth package:

npm i --save express-basic-auth

Add the requirement at the top of index.js:

const basicAuth = require("express-basic-auth");

Replace the app.use(adminJs.options.rootPath, router) block with the following:

app.use(adminJs.options.rootPath, basicAuth({
        users: { 'admin': 'supersecret' }, challenge: true,
        }), router);

This will tell Express to ask for a username and password combination (admin and supersecret respectively) when accessing the AdminJS root path. Now, when we start our application and head over to http://localhost:3000/admin, we should be greeted by a login dialog similar to that in Figure 10.1:

Figure 10.1 – Admin login

Figure 10.1 – Admin login

Now that our AdminJS routes are secure, we can start creating the frontend page that our customers will see when they visit the application.

Note

In a real-world scenario application, instead of using basic authentication, we would use another form of authentication such as JSON Web Tokens or a Single Sign-On Service.

Creating a page to list and book flights

For this application, we will be requiring two external libraries to help build the frontend components for the application. The first library is Bulma, which is a CSS framework designed for quick prototyping and doesn’t require its own JavaScript library. For more information on Bulma, you can visit its website, located at https://bulma.io/. The next library is AlpineJS, which is a framework that helps us avoid writing JavaScript to modify states or behaviors by using HTML tags and markup. More information can be found at https://alpinejs.dev/.

Note

Other fantastic frontend frameworks that can be used instead of AlpineJS include VueJS, React, or Deepkit. AlpineJS was chosen for this book due to its minimal setup and requirements.

Let us start with the bare necessities, the HTML for a simple header section of the website:

  1. Within public/index.html, add the following code:
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>Welcome to Avalon Airlines!</title>
      <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css">
      <script src="https://unpkg.com/[email protected]/dist/cdn.min.js" defer></script>
    </head>
    <body>
      <section class="section">
        <div class="container">
          <h1 class="title">
            Welcome to Avalon Airlines!
          </h1>
          <p class="subtitle">
            Where would you like to go 
             <strong>today</strong>?
          </p>
        </div>
      </section>
    </body>
    </html>
  2. After the first <section>, we will want to add another with a container separated by two columns as follows:
      <section class="section">
        <div class="container">
          <div class="columns" x-data="{
                        flights: [],
                        selected: {}
                      }" x-init="fetch('/flights')
                          .then(res => res.json())
                          .then(res => flights = res)">
            <div class="column">
            </div>
            <div class="column">
          </div>
        </div>
      </section>

The x-data attribute will tell AlpineJS what kind of shape our model and data will hold. This data will be propagated down to children elements. The x-init attribute will run upon initialization of the element and will fetch from our API calling /flights. Afterward, we take the results and convert them into a JSON object and then we assign the JSON response to the flights array within our x-data attribute.

  1. In the first column, from the section that we just created, we will want to create a table that renders all of the available flights:
    <table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth">
        <thead>
          <tr>
            <th>Origin</th>
            <th>Departure</th>
            <th>Departure Time</th>
            <th>Model</th>
            <th></th>
          </tr>
        </thead>
        <tbody>
          <template x-for="flight in flights">
            <tr>
              <td x-text="flight.originAirport"></td>
              <td x-text="flight.destinationAirport"></td>
              <td x-text="flight.departureTime"></td>
              <td x-text=
               "flight.Airplane.planeModel"></td>
              <td><button x-on:click="selected = flight" 
               class="button is-primary is-light is-
               small">Book
                  Flight</button></td>
            </tr>
          </template>
        </tbody>
      </table>

AlpineJS will recognize the x-for attribute, which operates similarly to for loops in other languages – anything inside of that block will be rendered for each iteration. If the flights array is empty, then the template block will not be rendered. The x-on:click attribute will add a click event listener to the button element, which will assign the selected variable (part of our x-data model from the parent element) to the associated flight entry.

  1. Next, we will want to create the logic for handling our form submission. Just above the closing body tag (</body>), we will want to add the following:
    <script>
      function flightForm() {
        return {
          data: {
            email: "",
            name: "",
            seat: "",
            success: false,
          },
          formMessages: [],
          loading: false,

The data, formMessages, and loading variables are all states for AlpineJS. We can choose whatever names we want, as it does not matter for AlpineJS.

  1. Now, for the submission event handling part, just below the loading: false block, add the following:
          submit(e) {
            this.loading = true;
            fetch("/book-flight", {
              method: "POST",
              headers: {
                "Content-Type": "application/json",
                "Accept": "application/json",
              },
              body: JSON.stringify({
                ...this.data,
                scheduleId: this.selected.id,
              }),
            })

Once the submit event has been invoked, a POST /book-flight request is made with the necessary JSON headers and body parameters. The this.selected.id variable will reference our parent’s element’s x-data model.

  1. After the fetch, we will need to handle the appropriate responses. Let us start with a successful path and add the following code just after the fetch block:
              .then(async (response) => {
                const { headers, ok, message, body } = 
                 response;
                const isJson = headers.get('content-
                 type')?.includes('application/json');
                const data = isJson ? await 
                 response.json() : await response.text();
                if (!ok) {
                  return Promise.reject(isJson ? 
                  Object.values(data) : data);
                }
               // boarding ticket was successfully created
                this.formMessages = [];
                this.data = {
                  email: "",
                  name: "",
                  seat: this.data.seat,
                  success: true,
                }
              })

This method will check whether the data is JSON or plain text. Then, it will check whether the response is OK (and return a rejected promise if it returned errors). If the ticket was successfully created, we will reset the email, name, and seat to their initial values and set success to true.

Note

We are setting the name and email to empty strings in the previous example to clear out the current form’s data. If we were to omit these explicit values, then AlpineJS would show the name and email inputs with their previous values when the flightForm appears on the screen.

  1. After that, we can add the catch and finally blocks and close the remaining script:
              .catch((err) => {
                this.formMessages = Array.isArray(err) ? 
                 err : [err];
              })
              .finally(() => {
                this.loading = false;
              });
          },
        };
      }
    </script>

The caught error will propagate itself to formMessages as an array and regardless of success or failure, we will want to use the finally block to set the loading state to false.

  1. Let’s return to the section with the two columns that we created earlier – in the second column, we will want to add a success message as well as the form itself. We will start with a section that displays information about the currently selected flight for our form:
    <div x-show="!!selected.id">
      <section class="hero is-info">
        <div class="hero-body">
          <p class="title">
            <span x-text="selected.originAirport"></span> &#8594; <span x-text="selected.destinationAirport">
            </span>
          </p>
          <p class="subtitle">
            Departs at <span x-text="selected.
            departureTime"></span>
          </p>
        </div>
      </section>

The x-show attribute will hide an element if the value yields as true. The next few elements will use the data from our selected object property from the parent element’s x-data model. This element should be hidden until we select a flight. The x-text attribute will tell AlpineJS to render the element’s innerText to the value associated with the attribute (for example, selected.originAirport, or selected.departureTime).

  1. Once the hero section is setup, we will add a form for the success message when a flight is successfully booked:
    <form x-data="flightForm()" @submit.prevent="submit">
      <div x-show="!!data.success">
        <section class="hero is-primary">
          <div class="hero-body">
            <p class="title">
              Your boarding ticket has been created!
            </p>
            <p class="subtitle">
              Your seat for this flight is <span 
               x-text="data.seat"></span>
            </p>
          </div>
        </section>
        <div class="mt-4 field is-grouped is-grouped-  
         centered">
          <p class="control">
            <a class="button is-light" 
              x-on:click="selected = {}; data.success =     
              false; data.seat = ''">
              OK
            </a>
          </p>
        </div>
      </div>

We encapsulated the states of flightForm and the events within the <form> tag. The @submit.prevent="submit" attribute will tell AlpineJS to prevent bubble propagation when submitting the event and to use our submit function inside of the flightForm method.

Next, we will check to see whether success is true and if so, show the order confirmation section. We will want some way to reset the state once a client has purchased a ticket (in case they want to purchase another ticket), which is what the x-on:click event does when we click the OK button.

  1. Now, for the actual form, we will check to see whether data.success is false and if so, show the form with some basic fields. Inside the same form attribute, add the following:
    <div x-show="!data.success">
      <div class="field pt-4">
        <label class="label">Full Name</label>
        <div class="control">
          <input class="input" type="text" x-model=
           "data.name" placeholder="e.g Alex Smith">
        </div>
      </div>
      <div class="field">
        <label class="label">Your Email</label>
        <div class="control">
          <input class="input" type="email" 
            x-model="data.email"
            placeholder="e.g. alexsmith@avalon-
            airlines.com">
        </div>
      </div>
      <div class="field">
        <label class="label">Seat Selection</label>
        <div class="control">
          <input class="input" type="text" 
            x-model="data.seat" placeholder="e.g. 1A">
        </div>
      </div>

The x-model attribute will bind the input’s value with the x-data object (for example, x-model="data.email" will associate itself with the data.email attribute of flightForm).

  1. Just below this code, we can add the call-to-action buttons for purchasing a ticket or canceling the order:
    <div class="field is-grouped is-grouped-centered">
      <p class="control">
        <button type="submit" :disabled="loading" 
        class="button is-primary">
          Purchase Ticket
        </button>
      </p>
      <p class="control">
        <a class="button is-light" x-on:click="selected = {}; 
        data.success = false; formMessages = []">
          Cancel
        </a>
      </p>
    </div>

The :disabled attribute is an AlpineJS shorthand code for disabling a particular element under a specific condition (in our case, this would be the loading variable). Clicking on the Cancel button will reset the selected data, set the data.success variable to false, and make formMessages into an empty array.

  1. Finally, we can add a template for handling our formMessages variable and close the remaining HTML tags:
                    <template x-for="message in 
                     formMessages">
                      <article class="message is-warning">
                        <div class="message-header">
                          <p>A correction is required</p>
                        </div>
                        <div x-text="message" class=
                         "message-body"></div>
                      </article>
                    </template>
                  </div>
                </form>

Our frontend application should now be complete. If we visit http://localhost:3000/, it should look similar to Figure 10.2. Clicking on the Book Flight button should generate something similar to Figure 10.3:

Figure 10.2 – Welcome to Avalon Airlines!

Figure 10.2 – Welcome to Avalon Airlines!

Figure 10.3 – Booking a flight

Figure 10.3 – Booking a flight

When we click on Purchase Ticket without entering any information, we should be greeted with a few warnings, as shown in Figure 10.4:

Figure 10.4 – Warnings from Sequelize

Figure 10.4 – Warnings from Sequelize

When we enter in the appropriate information, the application will create a new customer and boarding ticket along with a success message, as shown in Figure 10.5:

Figure 10.5 – The success message

Figure 10.5 – The success message

Visiting the admin dashboard will confirm that our ticket and customer account were created successfully. We can see the boarding tickets at http://localhost:3000/admin/resources/BoardingTickets (remember to log in with appropriate credentials), similar to Figure 10.6:

Figure 10.6 – The admin dashboard showing the boarding tickets

Figure 10.6 – The admin dashboard showing the boarding tickets

It looks as though our application is ready to be deployed. In the next section, we will go over the requirements for setting up an environment on a cloud application platform such as Fly.io.

Deploying the application

Before we begin, we will want to make sure our project is initialized as a git repository, if your machine does not have git installed you may find instruction on how to install the binary here https://git-scm.com/book/en/v2/Getting-Started-Installing-Git. If you’ve been following along, and haven’t yet initialized your project as a git repository, you can do so by running the following command in your project’s root directory:

git init

For the deployment process, we will use a cloud hosting service called Fly.io (https://fly.io/). Fly.io offers a useful command line tool to help us register and authenticate into an account in addition to making application deployments easier. Detailed instructions on getting started with Fly.io’s CLI can be found at https://fly.io/docs/hands-on/install-flyctl/.

For MacOS users, with Homebrew, we can install the binary with this command:

brew install flyctl

Linux users can install the binary with this command:

curl -L https://fly.io/install.sh | sh

For Window users, Fly.io recommends using the PowerShell for downloading the binary:

iwr https://fly.io/install.ps1 -useb | iex

Once the binary installation has been completed, we will need to login, or register a new account, and then create a new application. If you have not created your free Fly.io account previously, we can use the following command to get started

flyctl auth signup

Alternatively, we can authenticate ourselves if we had registered an account previously:

flyctl auth login

After we have authenticated, we can now deploy our application:

flyctl launch

This command will ask us for an application name and region which we can leave these values as blank or its default value. We will also be asked if we want to create a Postgres database and deploy the application right away which we should decline by entering in the “n” key as a response. The following should look similar to your screen:

Creating app in /Users/daniel/Documents/Book/code/ch10
Scanning source code
Detected a NodeJS app
Using the following build configuration:
	Builder: heroku/buildpacks:20
? App Name (leave blank to use an auto-generated name):
Automatically selected personal organization: Daniel Durante
? Select region: iad (Ashburn, Virginia (US))
Created app nameless-shape-3908 in organization personal
Wrote config file fly.toml
? Would you like to set up a Postgresql database now? No
? Would you like to deploy now? No
Your app is ready. Deploy with `flyctl deploy`

Don’t deploy the application just yet. We will need to enable MySQL with our Fly.io application first. At the moment, Fly.io does not offer a way to sidecar a MySQL database within the same application as our web application. The solution for this is to create a separate a Fly.io application with MySQL only.

In the project’s root directory, we will want to create a new folder called, “fly-mysql” and run the following command within that folder:

fly launch

Respond to the questions the same way we originally did in the previous fly launch command. Now, our database will need to be stored somewhere, so let us begin by creating a volume on Fly.io and choosing the same region as the previous step. Within the fly-mysql directory run the following command to create a new volume:

fly volumes create mysqldata --size 1

Note

The “--size” parameter for fly volumes create <name> references the number of gigabytes as its unit. For more information about the volumes Fly.io subcommand more information can be found at https://fly.io/docs/reference/volumes/.

Now, we can set our passwords for the MySQL instance (replace “password” with something more appropriate):

fly secrets set MYSQL_PASSWORD=password MYSQL_ROOT_PASSWORD=root_password

Throughout this process, Fly.io has created a fly.toml file for its applications (one for our web application in the project’s root directory and another for MySQL in the fly-mysql directory). This is similar to Heroku’s Procfile or CloudFlare’s wrangler.toml file. Within the fly.toml file we will want to replace its contents, after the first line (the application’s name) or starting from the kill_signal line, with the following:

kill_signal = “SIGINT”
kill_timeout = 5
[mounts]
  source=”mysqldata”
  destination=”/data”
[env]
  MYSQL_DATABASE = “avalon_airlines”
  MYSQL_USER = “avalon_airlines”
[build]
  image = “mysql:5.7”
[experimental]
  cmd = [
    “--default-authentication-plugin”,
    “mysql_native_password”,
    “--datadir”,
    “/data/mysql”
  ]

After modifying the file’s contents, we can scale our MySQL application to have 256 MB of RAM and deploy the MySQL instance:

fly scale memory 256
fly deploy

Now, going back to the project’s root directory, we can add a DATABSE_URL environment secret to our web application’s Fly.io configuration by running the following command:

flyctl secrets set DATABASE_URL=mysql://avalon_airlines:<YOUR PASSWORD>@<YOUR MYSQL’S APPLICATION NAME>.internal/avalon_airlines

Replace YOUR_PASSWORD with the password that was previously set for the MySQL’s application’s MYSQL_PASSWORD secret. Your MySQL’s application name should be available in the fly-mysql/fly.toml file marked with the app key.

Note

If you lose track of your application’s names, the Fly.io CLI provides a way to list all of your account’s application using the flyctl apps list command.

We will need to make some modifications to the package.json file. Since the application’s builder is using Heroku’s buildpacks, the application will be built with whatever the latest Long-Term Supported (LTS) Node.js version exist. The builder will also run the start script by default which currently uses nodemon. We can ensure the application is built with the proper Node.js version, and removing the nodemon dependency by replacing the start script within package.json to look like the following:

  “scripts”: {
    “start”: “node index.js”,
    “dev”: “nodemon index.js”
  },
  “engines”: {
    “node”: “16.x”
  },

Now, for when we are developing the application locally, we will want to execute npm run dev instead of npm run start.

Note

More information, and caveats, for Heroku’s Node.js buildpack can be found at https://devcenter.heroku.com/articles/nodejs-support.

From the Avalon Airlines project, we would need to open and modify the config/index.js file and replace the production object with the appropriate database connection values:

    “production”: {
        “use_env_variable”: “DATABASE_URL”,
        “dialect”: “mysql”
    }

Fly.io will deploy within a container cluster that exposes ports from a dynamic range. Due to this stipulation, we are required to modify the app.listen(3000, …) at the bottom of index.js:

app.listen(process.env.PORT || 3000, function () {
    console.log(“> express server has started”);
});

This will use the PORT environment variable, and default to a value of 3000 if the environment variable is not found, exposing our Express application properly on Fly.io’s ecosystem. There is one more change on the project root directory within the fly.toml file we will need to replace the [env] block with the following:

[env]
  PORT = “8080”
  NODE_ENV = “production”

Everything else should remain the same, and now, we can deploy and open our application:

flyctl deploy
flyctl open

Note

You may receive a similar error as, “Cannot find module 'sequelize',” this can be from a third-party application dependency such as Admin.js. As a temporarily solution we can manually install, and save, the original Sequelize library by entering npm i sequelize into your terminal within the projects directory and re-deploy your application.

You may notice that the website looks a little bare, we can head over to the /admin dashboard route and start populating our airplane inventory and flight schedules. Once that is done, we can start processing and booking tickets for Avalon Airlines!

Figure 10.7 – The Avalon Airlines homepage with a flight scheduled!

Figure 10.7 – The Avalon Airlines homepage with a flight scheduled!

Summary

In this chapter, we went through the process of adding a frontend page with the ability to generate a list of flight schedules and create boarding tickets. We also learned how to deploy our application to a cloud application environment.

Congratulations! We have completed the process of becoming familiar with Sequelize to deploying a Sequelize-based web application. In a real-world scenario, we would want to make a few more adjustments, such as securely storing database credentials, setting up transactional emails, adding more pages, processing credit cards, and having an actual seating inventory management system. At this point, the rest is up to you and only the sky is the limit! Hopefully, this will be a satisfying start for you! It certainly should be, because the Avalon Airlines board members are pleased so far, and they’ve decided to fund our next round.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset