Travel App Walk-through

Travel App Walk-through

The travel app is a single-page web application built using a decoupled AngularJS front-end that communicates with the application by using REST endpoints.

Architecture

The application serves an HTML file named index.html from the public path in the Node.js application. This file contains references to various included script files for front-side JavaScript components such as Bootstrap, jQuery, and AngularJS. The Angular controller script named script.js, which you can find in the /public/js/ directory, controls how the application interacts with the Node.js REST API in the server application.

Figure 1. The Application Services

REST API

The application uses these methods to populate data in the application:

  • $scope.findFlights, which is a REST API call to the /api/flightPath/findAll endpoint
  • $scope.findAirports, which is a REST API call to the /api/airport/findAll endpoint
Figure 2. Front End Framework REST calls
Figure 3. REST API for finding airports

The application attempts to find an airport based on the codes used for the name (case sensitive), and codes for Federal Aviation Administration (FAA) or International Civil Aviation Organization (ICAO). The travel app uses the data model and binds the input field for the from airport to an Angular typeahead directive, which is defined in the index.html file as follows:

Angular typeahead directive from /public/index.html file
<input type="text" 
    placeholder="find airport"
    typeahead="data.airportname for data in findAirports($viewValue)"
    typeahead-min-length="3"
    typeahead-wait-ms="250"
    ng-model="fromName"
    ng-minlength="3"
    class="input-small form-control"
    name="from" required/>

In the Angular controller script, the findAirports() function is:

findAirports() function from /public/js/scripts.js file
$scope.findAirports=function(val){
    return $http.get("/api/airport/findAll",{
        params:{search:val}
    }).then(function(response){
        return response.data;
    });
}

The API call to the server application /api/airport/findAll function is defined in the /routes/routes.js file as:

/api/airport/findAll function from /routes/routes.js file
//// ▶▶ airports ◀◀ ////
app.get('/api/airport/findAll',function(req,res) {
  if (req.query.search) {
      airport.findAll(req.query.search, function (err, done) {
          if (err) {
              res.status = 400;
              res.send(err);
              return;
          }
          res.status = 202;
          res.send(done);
      });
  }else{
      res.status = 400;
      res.send({"airport":"bad request"});
      return;
  }
});

The /api/airport/findAll route function in the route.js file points to the corresponding findAll function in the airport module in the /model/airport.js file. This method allows the user to search by FAA code, ICAO code or airport name. It then prepares a N1QL query based on the selection criteria and bucket information from the /config.json file. The query is passed to the Couchbase query service in the /model/db.js module that calls Couchbase.

findAll() function from /model/airport.js file
module.exports.findAll = function (queryStr, done) {
    var queryPrep;
    if (queryStr.length == 3) {
        queryPrep = "SELECT airportname FROM `" + config.couchbase.bucket + "` WHERE faa ='" + queryStr.toUpperCase() + "'";
    } else if (queryStr.length == 4 && (queryStr==queryStr.toUpperCase()||queryStr==queryStr.toLowerCase())) {
        queryPrep = "SELECT airportname FROM `" + config.couchbase.bucket + "` WHERE icao ='" + queryStr.toUpperCase() + "'";
    } else {
        queryPrep = "SELECT airportname FROM `" + config.couchbase.bucket + "` WHERE airportname LIKE '" + queryStr + "%'";
    }

    db.query(queryPrep, function (err, res) {
        if (err) {
            done(err, null);
            return;
        }
        if (res) {
            done(null, res);
            return;
        }
    });
}

Using the data model, if you enter SEA, KSEA, or Seattle, the typeahead directive gives you the option to select Seattle Tacoma Intl. To see the results of the query, watch the terminal window in which the Node application is running. The example is showing an output for the N1QL statements:

QUERY: SELECT airportname FROM `travel-sample` WHERE faa ='SEA'
QUERY: SELECT airportname FROM `travel-sample` WHERE icao ='KSEA'
QUERY: SELECT airportname FROM `travel-sample` WHERE airportname LIKE 'Seattle%'

You can enable or disable console logging for N1QL statements by changing the value of the showQuery property in the /config.json file to true or false.

Figure 4. REST API for finding flight paths

After the airports are selected and a leave date is entered, the application tries to find route and schedule information for an airline that services the requested flight path. The call to the REST API /api/flightPath/findAll on the server is initiated when the user clicks the Find Flights button. The button is defined within the travelForm form element in the index.html file, as shown in the following code snippets:

travelForm form element from /public/index.html file
<form class="form-horizontal" role="form" 
       name="travelForm" ng-submit="findFlights()" novalidate>
Find Flights button element from /public/index.html file
<button class="btn btn-primary pull-right btn-sm" 
         type="submit" ng-disabled="travelForm.$invalid">Find Flights</button>

The travelForm form element contains various Angular validation options that are used with the input directives. For more information about validation and the input directive see https://docs.angularjs.org/api/ng/directive/input. After validating the input options, the Angular function in the script.js file is called to find flights.

findFlights() function from /public/js/scripts.js file
$scope.findFlights = function () {
    $scope.empty = true;
    $scope.rowCollectionLeave = [];
    $scope.rowCollectionRet = [];
    $http.get("/api/flightPath/findAll", {
        params: {from: this.fromName, to: this.toName, leave: this.leave}
    }).then(function (response) {
        if (response.data.length > 0) {
            $scope.empty = false;
        }
        for (var j = 0; j < response.data.length; j++) {
            $scope.rowCollectionLeave.push(response.data[j]);
        }
    });
    if (this.ret) {
        $http.get("/api/flightPath/findAll", {
            params: {from: this.toName, to: this.fromName, leave: this.ret}
        }).then(function (responseRet) {
            if (responseRet.data.length > 0) {
                $scope.retEmpty = false;
            }
            for (var j = 0; j < responseRet.data.length; j++) {
                $scope.rowCollectionRet.push(responseRet.data[j]);
            }
        });
    }
}

The findFlights() function checks the form input to determine whether the return option is enabled or disabled. It then calls the server REST API /api/flightPath/findAll function either once for a one way flight or twice for a round-trip flight. When it requests the return flight for a round trip, it uses the reverse to and from sequence.

/api/flightPath/findAll function from /routes/routes.js file

//// ▶▶ flightPath ◀◀ ////
app.get('/api/flightPath/findAll',function(req,res){
    if(req.query.from && req.query.to && req.query.leave){
        flightPath.findAll(req.query.from, req.query.to,req.query.leave, function (err, done) {
            if (err) {
                res.status = 400;
                res.send(err);
                return;
            }
            res.status = 202;
            res.send(done);
        });
    }else{
        res.status = 400;
        res.send({"flightPath":"bad request"});
        return;
    }
});

The /api/flightPath/findAll route function in the route.js file points to the corresponding findAll function in the flightPath module, found in the /model/flghtPath.js file. The findAll function performs several important steps for processing the request to find flights:

  • It determines the FAA identifier for the to and from airports.
  • It searches for routes and schedule information based on the source and destination airports and the requested dates. The collection of schedule documents is nested in the route document for each route. They are grouped by day of the week (1-7), and the requested dates are compared to the day of the week to see what flights are available on those particular dates. It returns a list of flights that includes data for the following fields: airline, flight, departure, from, to, and aircraft.

This method in the flightPath.js module is:

findAll function from the /model/flightPath.js file

module.exports.findAll = function (from, to, leave,done) {
    var queryPrep = "SELECT faa as fromAirport FROM `" + config.couchbase.bucket + "` WHERE airportname = '" + from +
        "' UNION SELECT faa as toAirport FROM `" + config.couchbase.bucket + "` WHERE airportname = '" + to + "'";
    db.query(queryPrep, function (err, res) {
        if (err) {
            done(err, null);
            return;
        }
        if (res) {
            var queryTo;
            var queryFrom;
            for(i=0;i<res.length;i++){
                if(res[i].toAirport){
                    queryTo=res[i].toAirport;
                }
                if(res[i].fromAirport){
                    queryFrom=res[i].fromAirport;
                }
            }
            queryPrep="SELECT a.name, s.flight, s.utc, r.sourceairport, r.destinationairport, r.equipment FROM `" + config.couchbase.bucket + "` r UNNEST r.schedule s JOIN `" + config.couchbase.bucket + "` a ON KEYS r.airlineid WHERE r.sourceairport='" + queryFrom + "' AND r.destinationairport='" + queryTo + "' AND s.day=" + convDate(leave) + " ORDER BY a.name";
            db.query(queryPrep,function (err, flightPaths) {
                         if (err) {
                             done(err, null);
                             return;
                         }
                         if (flightPaths) {
                             done(null, flightPaths);
                             return;
                         }
                     }
            );
        }
    });
}

The Query can be seen in the terminal window that is running the node.js application:

QUERY: 
SELECT a.name, s.flight, s.utc, r.sourceairport, r.destinationairport, r.equipment 
FROM `travel-sample` r 
UNNEST r.schedule s 
JOIN `travel-sample` a 
ON KEYS r.airlineid 
WHERE r.sourceairport='SEA' AND r.destinationairport='MCO' AND s.day=6 
ORDER BY a.name

N1QL Query Anatomy

The Couchbase Query API is a powerful tool for efficient retrieval of information from a document data store. In the travel application, queries about flights are created by the flightPath.js module. Here's an example of a query that finds flights between Seattle-Tacoma International Airport (SEA) and Orlando International Airport (MCO), followed by a description of what's happening in the query:

SELECT a.name, s.flight, s.utc, r.sourceairport, r.destinationairport, r.equipment 
FROM `travel-sample` r 
UNNEST r.schedule s
JOIN `travel-sample` a ON KEYS r.airlineid 
WHERE r.sourceairport='SEA' AND r.destinationairport='MCO' AND s.day=6 
ORDER BY a.name

N1QL provides JOIN functionality, something previously not possible in a document database. For two documents to be joined in the result of a SELECT statement, one of them must contain a field whose value is equal to the Couchbase key of the other document. The following example shows two documents that demonstrate that requirement and a SELECT statement that joins them:

"keyA" is the Couchbase KV key for Doc A.
Doc A: { some fields }
 
Doc B: { some fields "joinField": "keyA" }

SELECT * FROM default b JOIN default a ON KEYS b.joinField

The data model for the travel application includes an airlineid field in each route document. That airlineid field is used as a foreign key identifier and corresponds to the key for an airline document. To select the airline name a.name, the query uses the following clause: JOIN `travel-sample` a ON KEYS r.airlineid.

One of the most powerful features available in the Couchbase Query API is the ability to UNNEST or flatten,the results returned in the SELECT statement. In the data model for the travel application, each route document contains a nested collection of schedule documents. To alleviate a complicated JSON parsing code pattern for the return results, you can UNNEST the schedule documents, so they become the root-level fields in the returned results.

Standard SQL syntax is used in the WHERE clause for the SELECT statement. The result set is ordered by the a.name field, which contains the airline name.