Couchbase Travel Mobile is a sample mobile application that extends the capabilities of the Travel Sample Web Application to include the mobile platform. The Travel Sample app demonstrates the full stack capabilities of the Couchbase Platform and includes the following:

In addition to the components listed above, the travel sample mobile app leverages the Travel Sample Web Backend Application’s integration with Couchbase’s Query Services. Specifically, the Python version of the web application backend is used.

The mobile app is currently supported on the iOS platform. Support for other platforms is coming soon.

To start, it is easiest if you run all the components on the same machine. It’s not required to run your development environment this way, and advanced configurations are supported but it is more convenient to start a development environment with components running locally. (the default configuration assumes local installation)

Travel Sample Mobile App (iOS) w/ Couchbase Lite 2.0

  • Clone the iOS version of the app from GitHub

      git clone https://github.com/couchbaselabs/mobile-travel-sample.git
    
  • Run the install.sh script to download and install the 2.0 version of Couchbase Lite.

      cd ios/TravelSample
      sh install.sh
    
  • Launch the TravelSample.xcodeproj using Xcode
  • Update the Sync Gateway replication endpoint to the address of the Sync Gateway
    • Open DatabaseManager.swift file and locate the kRemoteSyncUrl constant.

        blip://demo:password@localhost:4984
      
    Update “localhost” to the address of Sync Gateway (if it’s different from localhost). The user with credentials “demo”/”password” corresponds to a user configured on the Couchbase Server and Sync Gateway. We will learn more about users in the “user configuration” section in this document.
  • Update the Web Backend endpoint to the address of the Travel Sample Backend
    • Open FlightPresenter.swift and locate serverBackendUrl

        http://localhost:8080/api/
      

      Update “localhost” to the address of Web Backend (if different from localhost)

  • Build and Run the app using Xcode. The current version of the app can be built with Xcode 8.3 and supports Swift 3.1.

  • If successful, you should see the app launch with the login screen.

Sync Gateway (v1.5)

Follow the instructions listed here to download v1.5 of Sync Gateway on your local machine.

The Travel Sample Mobile App GitHub repo includes the Sync Gateway config file named sync-gateway-config-travelsample.json that you must launch the Sync Gateway with.

cd /path/to/syncgatewayinstall/bin
./sync_gateway sync-gateway-config-travelsample.json

Couchbase Server 5.0 (Beta)

  • Download and install V5.0 of Couchbase Server
  • As you follow the download instructions and setup wizard, make sure you keep all the services (data, query, and index) selected.
  • Install the sample bucket named travel-sample because it contains the data used in this tutorial. If you have a previous installation of the server without this bucket, you can add the bucket from the “Sample Buckets” tab in the “Settings” menu in the admin console

  • Create an RBAC user named “admin” with password “password” with full access to the travel-sample bucket.

Travel Sample App Web Backend

We will use the Python version of the Travel Sample Web Backend which uses the Couchbase Python SDK to with with Couchbase Server Query Services.

  • Clone the repo and checkout the 5.0 Branch

      git clone https://github.com/couchbaselabs/try-cb-python.git
      git checkout 5.0
    
  • Follow the instructions specified in this guide to build and run the app.

System Architecture

Here are the main elements of the architecture

Document Import

Documents from the travel-sample bucket on Couchbase Server are imported through the Sync Gateway over the Database Change Protocol(DCP). Note that even if the document is directly added to the Couchbase server bucket through the SDK, they will be processed by the Sync Gateway for replication to mobile clients. For this, the Sync Gateway processes the imported documents and adds appropriate sync metadata to XAttrs which allows it to be replicated to the mobile clients. In the beta version of Sync Gateway, all documents are imported and processed by the Sync Function. In a future release, it would be possible to selectively filter the documents that get imported from the bucket.

The following configuration settings in the Sync Gateway config file enables the import.

"unsupported": {
	"enable_extended_attributes": true
}

Couchbase Lite 2.0

The travel sample mobile app is integrated with Couchbase Lite 2.0 that provides embedded storage capabilities. The app interfaces with the new Couchbase Lite 2.0 query interface to perform N1QL like queries into the local database. Documents in Couchbase Lite 2.0 are stored in Fleece format, which is binary encoding for JSON.

Replication (between Mobile Clients and Sync Gateway)

Couchbase Lite synchronizes with the Sync Gateway over BLIP, the new messaging protocol over WebSockets. Documents are pushed/pulled over BLIP over a secure, authenticated transport.

The Sync Gateway replication end point will be blip://localhost:4984 (assuming the sync gateway is installed on a local machine). The following configuration settings in the Sync Gateway config file enables replication over BLIP.

"unsupported": {
	"replicator_2":true
}

Prebuilt Embedded Database

The mobile app includes a pre-built/canned .cblite database that contains all the airport and hotel documents. It is conceivable that in a real world, semi-static documents are bundled with the app and available even when devices are offline. This implies that searches for hotels and airports will be queries against the local database.

REST API to Travel Sample Web Backend

The Travel Sample Python Web Backend exposes a RESTful interface that allows web and mobile apps to query into the Couchbase Server bucket. The travel sample mobile app uses this interface to search for matching flights based on specific search criteria.

User Configuration

The “admin” RBAC user is created on the Couchbase Server as dicussed in the “Installation” section. Users are created through the Travel sample web app

By default, a user of “demo” with password “password” is configured in the Sync Gateway config file. So it may be simpler if you go ahead and create a user with the same credentials.

The users are then manually configured in the Sync Gateway via the configuration file.

"users": {
	"admin": {"password": "password", "admin_channels": ["*"]}, 
	"demo": {"password": "password"},
	"tester": {"password": "password"}
}

Access Control

A user can create, edit, delete flight bookings associated with self.

Data Model

There are four types of documents - “Hotel”, “Airport”, “Airline” and “Route”. These contain static data. The “Hotel” and “Airport” documents are included in the prebuilt couchbase lite database bundled with the mobile app.

In addition, every user is associated with a user document with document ID of the form "user::"<user_name> where user_name is the name of user used when creating an account. This user document gets created when a user signs up via the travel sample web app. The user document contains the list of all flight bookings along with the “username” and “password”. It can be edited from either the travel sample web app or the mobile app and changes are synchronized to both apps.

You can find a detailed description of the data model here

Sync Function

The Sync Function performs basic validation of the user document. All user documents, whether they are created via the web app or the mobile app will be processed by the Sync Function which will assign the user documents to user specific channels. The Mobile app will only fetch documents from the logged in user’s channel.

Documents in the travel-sample bucket other than the user document are ignored by the Sync Function.

function sync(doc, oldDoc) {
    /* Just ignore all the static travel-sample files */
    var type = getType();
    if (type == "hotel" || type == "airport" || type =="airline" || type == "route") {
         return;
    } 
    /* validation */
    var username = getUserName();
    // Verify the user making the request is the same as the one in doc's username
    requireUser(doc.username);

   if (!isDelete()) {
      // Validate required fields.
      validateNotEmpty("username", doc.username);

      if (isCreate()) {

        // Validate that the _id is prefixed by owner.
        var expectedDocId = "user" + "::" + doc.username;

        if (expectedDocId != doc._id) {
            throw({forbidden: "user doc Id must be of form user:userId"}); 
        }
      
      } else {
         // Validate that the username hasn't changed.
        validateReadOnly("username", doc.username, oldDoc.username);
      }
    }


  /* Routing */
  // Add doc to the user's channel.
  channel("channel." + username);
 
  // Give user read access to channel
   if (!isDelete()) {
    // Deletion of user document is essentially deletion of user
       access(username,"channel." + username)
    }

  function getType() {
    return (isDelete() ? oldDoc.type : doc.type);
  }

  function getUserName() {
    return (isDelete() ? oldDoc.username : doc.username);
  }

  function isCreate() {
    // Checking false for the Admin UI to work
    return ((oldDoc == false) || (oldDoc == null || oldDoc._deleted) && !isDelete());
  }

  function isUpdate() {
    return (!isCreate() && !isDelete());
  }

  function isDelete() {
    return (doc._deleted == true);
  }

  function validateNotEmpty(key, value) {
    if (!value) {
      throw({forbidden: key + " is not provided."});
    }
  }

  function validateReadOnly(name, value, oldValue) {
    if (value != oldValue) {
      throw({forbidden: name + " is read-only."});
    }
  }

}

Mobile Workflow

In this section, we will discuss the user workflow through the mobile app. For a discussion of the workflow via the Travel Sample web app, please refer to the corresponding web app documentation.

  • User logs into app. As discussed in the “User Configuration” section, the user Account is created via the Travel Sample Web App. User sign up via the mobile app is not supported.
  • User views all flight bookings created by self in the “Bookings” screen. This view will include flight bookings created from either the web app or mobile app.
  • User creates a new flight booking

    • Enter departure and arrival airports: airport lookup is done via Full Text Search query on the local prebuilt Couchbase Lite DB and is used to populate drop-down menu.
    • Enter departure and/or return dates: when the lookup button is tapped, a N1QL query is made against Couchbase server via the Web Backend SDK

  • User makes outbound and/or inbound flight selection and taps Confirm Booking
  • The booking shows up in the Bookings screen

  • User searches for Hotels
    • Enter specific search criteria (location, description) and tap lookup. When the lookup button is tapped, an FTS query is made against a local Couchbase Lite database and returns a list of matching hotels

Queries

Airport Search Query into Couchbase Lite

Open the AirportPresenter.swift file and look for fetchAirportsMatching function. Depending on the number of characters in the search string, the query searches by FAA code, ICAO code or does an FTS query into the local Couchbase Lite Database.

var searchQuery:Query?
        switch searchStr.characters.count {
        case AirportCodeLength.FAA.rawValue :
            searchQuery = Query
                .select()
                .from(DataSource.database(db))
                .where(Expression.property("type")
                    .equalTo("airport")
                    .and(Expression.property("faa")
                    .equalTo(searchStr.uppercased())))
            
        case AirportCodeLength.ICAO.rawValue:
            searchQuery = Query
                .select()
                .from(DataSource.database(db))
                .where(Expression.property("type")
                    .equalTo("airport")
                    .and(Expression.property("icao")
                    .equalTo(searchStr.uppercased())))
        default:
            // Search for all airports starting with specific searchStr
            searchQuery = Query
                .select()
                .from(DataSource.database(db))
                .where(Expression.property("type")
                    .equalTo("airport")
                    .and (Expression.property("airportname")
                    .like("\(searchStr)%")))
        }
        if let searchQuery = searchQuery {
            var matches:Airports = []
            do {
                for row in try searchQuery.run() {
                    if let match = row.document.string(forKey: "airportname") {
                        matches.append( match)
                    }
                }
                handler(matches,nil)
            }
            catch {
                handler(nil,error)
            }
        }

Hotel Search Query into Couchbase Lite

Open the HotelPresenter.swift file and look for the fetchHotelsMatchingDescription function. This function performs an FTS into the local Couchbase Lite database.

 if let descriptionStr = descriptionStr {
            descExp = Expression.property("description").like("%\(descriptionStr)%")
                            .or(Expression.property("name").like("%\(descriptionStr)%" ))
        }
      
        
        let locationExp = Expression.property("country").equalTo(locationStr)
            .or(Expression.property("city").equalTo(locationStr))
            .or(Expression.property("state").equalTo(locationStr))
            .or(Expression.property("address").equalTo(locationStr))
        
        var searchExp:Expression = locationExp
        if  let descExp = descExp {
            searchExp = locationExp.and(descExp)
        }

        let hotelSearchQuery = Query
            .select()
            .from(DataSource.database(db))
            .where(Expression.property("type")
                .equalTo("hotel")
            .and(searchExp))
        
           print(try! hotelSearchQuery.explain())
    
        var matches:Hotels = []
        do {
            for (index,row) in try hotelSearchQuery.run().enumerated() {
                
                let match = row.document.toDictionary()
                matches.append(match)
                
            }
            handler(matches,nil)
        }
        catch {
            handler(nil,error)
        }

Bookings Query into Couchbase Lite

Open the BookingPresenter.swift file and look for the findBookingsForCurrentUser function. This query looks up documents for the specified username from the Couchbase Lite database.

let bookingQuery = Query
		.select()
		.from(DataSource.database(db))
		.where(Expression.property("username").equalTo(user)) // Just being future proof.We do not need this since there is only one doc for a user and a separate local db for each user anyways.
try! print(bookingQuery.explain())
		
do {

		for (_, row) in try bookingQuery.run().enumerated() {
				// There should be only one document for a user
				print (row.document.array(forKey: "flights")?.toArray() ?? "No element with flights key!")
				if let bookings = row.document.array(forKey: "flights")?.toArray() as? Bookings {
						 _bookings += bookings
				}
		 }

} catch {
		 
		print(error.localizedDescription)
}

Flights Lookup Query into Couchbase Server

Open the FlightPresenter.swift file and look for the fetchFlightsForCurrentUserWithSource function. This function makes an HTTP request to the REST interface exposed by the Travel Sample web app to query for matching flights. The Travel sample web app uses the SDK to do a N1QL query into Couchbase Server.

  • For example, the following query is used by to look up for available flights from Heathrow to San Diego leaving on 05/04/2017 and returning on 05/05/2017

      http://localhost:8080/api/flightPaths/Heathrow/San%20Diego%20Intl?leave=05/04/2017&return=05/05/2017
    

Refer to this guide for a full description of the REST API specification.