Sample Application
Edit this article in GitHub
Version 2.2

Sample Application

Couchbase Travel is a sample web application that demonstrates how to interact with the Couchbase query service via the SDKs

Each SDK comes with its own implementation of the backend for the application. You can download the complete source code and then build and run the app. While the app runs, you can view the log of N1QL queries that it sends. The documentation for the travel app illustrates the data model and walks through the N1QL queries used to select flights.

The travel app front-end is the same in each SDK implementation and incorporates the following frameworks:

The backend is a REST API, and is available in the following languages:

Please refer to the travel-app documentation of your SDK of choice for specifics for that particular SDK, like backend dependencies, source-code checkout and running instructions.

The application allows users to find flights by entering airports and travel dates. For the airport entries, the app uses a N1QL query bound to an Angular type ahead directive to suggest airport names based on the first few letters entered.

If you want to try the sample app first, without having to build and run the sample code, take a look at the travel app hosted at http://try.couchbase.com. See Using the Travel App below for guidelines on how to use the travel app.

Generic set up

You'll need:
  • Your favorite editor or IDE
  • Your SDK of choice and its specific dependencies
  • The sample app source code for your SDK from GitHub
  • A local Couchbase 4.5 installation (make sure that the travel-sample bucket has been loaded and that there is, at least, one node with data, query, index and search services in the cluster
  • That's it!

To start with, it's easiest if you run Couchbase Server and the travel sample app on the same machine. It's not required to run your development environment this way, and advanced "MDS" configurations are supported. It's just easier to start a development environment with components running locally.

Download Couchbase Server 4.5 and install it. As you follow the download instructions and setup wizard, make sure you keep all the services (data, query, and index) selected. Make sure also to install the sample bucket named travel-sample (introduced in CB 4.0) because it contains the data used in this tutorial.

If you already have Couchbase Server installed but did not install the travel-sample bucket, open the Couchbase Web Console and select Settings > Sample Buckets. Select the travel-sample checkbox, and then click Create. A notification box in the upper-right corner disappears when the bucket is ready to use.

The travel app, as a REST API backend, will run in the terminal window, which you need to keep open. Please refer to SDK-specific instructions on how to run the application.

The Travel App data model

The data model for the travel app uses several distinct document types: airline, route, airport and landmark.

The model for each kind of document contains:
  • A key that acts as a primary key
  • An id field that identifies the document
  • A type field that identifies the kind of document

The following figure illustrates the relationship between the different kinds of documents. It shows the primary key, ID, and type fields that each document has, plus a few representative fields in each type of document.

Figure 1. Documents in the travel app data model

Airline documents

Airline documents contain details about airlines such as the name of the airline, International Air Transport Association (IATA) two-character airline designator, International Civil Aviation Organization (ICAO) three-character airline designator, and the airline call sign.

For airline documents, the value of the type field is airline.

Airline document model
airline_24 			   ←This is the key, which also acts as a primary key
{
  "active": "Y",
  "callsign": "AMERICAN",
  "country": "United States",
  "iata": "AA",
  "icao": "AAL",
  "id": "24",
  "name": "American Airlines",
  "type": "airline"              ←This is the type identifier for the document
}

Route documents

Route documents contain details about flights such as the name of the airline, departure airport, destination airport, number of stops during the flight, type of aircraft, flight number, and flight schedule.

Route documents also contain a foreign key identifier, airlineid, that is used to retrieve the document that contains information about the airline that flies the route. The value of the airlineid field is identical to the key for the corresponding airline document.

For route documents, the value of the type field is route.

Route document model
route_5966                         ←This is the key, which also acts as a primary key
{
  "id": "5966",
  "type": "route",               ←This is the type identifier for the document
  "airline": "AA",
  "airlineid": "airline_24",     ←This is the foreign key identifier to an airline document
  "sourceairport": "MCO",
  "destinationairport": "SEA",
  "stops": "0",
  "equipment": "737",
  "schedule": [
      {"day": 1, "utc": "13:25:00", "flight": "AA788"},
      {"day": 4, "utc": "13:25:00", "flight": "AA419"},
      {"day": 5, "utc": "13:25:00", "flight": "AA519"}
  ]
}

Airport documents

Airport documents contain details about airports such as name, location, time zone, ICAO four-character alphanumeric airport code, and Federal Aviation Administration (FAA) location identifier.

For airport documents, the value of the type field is airport.

Airport document model
airport_3577                       ←This is the key, which also acts as a primary key
{
  "travel-sample": {
      "airportname": "Seattle Tacoma Intl",
      "city": "Seattle",
      "country": "United States",
      "faa": "SEA",
      "geo": {
          "alt": 433,
          "lat": 47.449,
          "lon": -122.309306
      },
      "icao": "KSEA",
      "id": 3577,
      "type": "airport",         ←This is the type identifier for the document
      "tz": "America/Los_Angeles"
  }
}

Landmark documents

Landmark documents contain details about points of interest such as hotels. They include information such as name, location, price, contact information, and the kind of activity that the point of interest provides.

For landmark documents, the value of the type field is landmark.

Landmark document model
landmark_21661                     ←This is the key, which also acts as a primary key
{
  "activity": "sleep",
  "address": "12 Rue Boulainvilliers",
  "alt": null,
  "checkin": null,
  "checkout": null,
  "city": null,
  "content": "Small three star hotel (33 rooms).",
  "country": "France",
  "directions": null,
  "email": null,
  "fax": null,
  "geo": {
      "lat": 48.853,
      "lon": 2.27593
  },
  "hours": null,
  "id": 21661,
  "image": null,
  "name": "Hotel Eiffel Kennedy",
  "phone": "+33 1 45 24 45 75",
  "price": "~\u20ac150",
  "state": "Ile-de-France",
  "title": "Paris/16th arrondissement",
  "tollfree": null,
  "type": "landmark",            ←This is the type identifier for the document
  "url": "http://ww.eiffelkennedy.com"
}

Architecture

The application serves an HTML file named index.html (eg. 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 REST API in the server application.

Figure 2. 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 3. Front End Framework REST calls
Figure 4. 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 5. 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 backend 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 each SDK-specific travel application, queries about flights are created by a dedicated module (for example in Node.js, 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.

Using the Travel App

  1. Open a browser and navigate to the login URL that was displayed when you started the app.
  2. Sign in to Couchbase Travel by providing your credentials:

  3. Find a flight:

    In the Airport or City section, enter an airport code or city name in the From and To fields.

  4. In the Travel Dates section, select Leave and Return dates by using the date picker that pops up when you click the date fields.
  5. Click Find Flights.

    The app displays the available flights for the outbound and return legs of the trip.