RhoConnect Source Adapters === Connecting to a backend service with RhoConnect requires that you write a small amount of Ruby code for the query, create, update and delete operations of your particular enterprise backend. The collection of the Ruby code for these operations we refer to as a "source" or "source adapter". ## Generate Source Adapter Adding a new source adapter to your RhoConnect application is simple: :::term $ cd storeserver $ rhoconnect source product This will generate a new ruby class called `sources/product.rb`: :::ruby class Product < SourceAdapter def initialize(source) super(source) end def login # TODO: Login to your data source here if necessary end def query # TODO: Query your backend data source and assign the records # to a nested hash structure called @result. For example: # @result = { # "1"=>{"name"=>"Acme", "industry"=>"Electronics"}, # "2"=>{"name"=>"Best", "industry"=>"Software"} # } raise SourceAdapterException.new("Please provide some code to read records from the backend data source") end def sync super end def create(create_hash) # TODO: Create a new record in your backend data source # If your rhodes rhom object contains image/binary data # (has the image_uri attribute), then a blob will be provided raise "Please provide some code to create a single record in the backend data source using the create_hash" end def update(update_hash) # TODO: Update an existing record in your backend data source raise "Please provide some code to update a single record in the backend data source using the update_hash" end def delete(delete_hash) # TODO: write some code here if applicable # be sure to have a hash key and value for "object" # for now, we'll say that its OK to not have a delete operation # raise "Please provide some code to delete a single object in the backend application using the object_id" end def logoff # TODO: Logout from the data source if necessary end end It also adds a spec file `spec/sources/product_spec.rb` and updates `settings/settings.yml` with the product adapter to the sources section with some default options: :::yaml :sources: Product: :poll_interval: 300 ## Source Adapter API The source adapter interface is described below, your source adapter can use any of these methods to interact with your backend service. ### `login` Login to your backend service (optional). :::ruby def login MyWebService.login(current_user.login) end ### `logoff` Logoff from your backend service (optional). :::ruby def logoff MyWebService.logoff(current_user.login) end ### `query(params = nil)` Query your backend service and build a hash of hashes (required). **NOTE: This method must assign `@result` to a hash of hashes. If @result is `nil` or `{}`, the master document will be erased from redis.** :::ruby def query parsed = JSON.parse(RestClient.get("#{@base}.json").body) @result = {} parsed.each do |item| @result[item["product"]["id"].to_s] = item["product"] end if parsed end ### `search(params)` Search your backend based on params and build a hash of hashes (optional). Similar to query, however the master document accumulates the data in `@result` instead of replacing when it runs. :::ruby def search(params) parsed = JSON.parse(RestClient.get("#{@base}.json").body) @result = {} parsed.each do |item| if item["product"]["name"].downcase == params['name'].downcase @result[item["product"]["id"].to_s] = item["product"] end end if parsed end Next, you will need to add search to your Rhodes application. For details, see the [Rhodes search section](/rhodes/synchronization#filtering-datasets-with-search). ### `create(create_hash)` Create a new record in the backend (optional). **NOTE: RhoConnect can establish a 'link' between the local record id provided by the client and the new record id provided by the backend service. To enable this link, return the new record id as a string.** :::ruby def create(create_hash) res = MyWebService.create(create_hash) # return new product id so we establish a client link res.new_id end ### `update(update_hash)` Update an existing record in the backend (optional). :::ruby def update(update_hash) end ### `delete(delete_hash)` Delete an existing record in the backend (optional). :::ruby def delete(delete_hash) MyWebService.delete(delete_hash['id']) end ### `current_user` Returns the current user which called the adapter. For example, you could filter results for a specific user in your query method: :::ruby def query @result = MyWebService.get_records_for_user(current_user.login) end ### `stash_result` Saves the current state of `@result` to redis and assigns it to `nil`. Typically this is used when your adapter has to paginate through backend service data. :::ruby def query @result = {} ('a'..'z').each_with_index do |letter,i| @result ||= {} @result.merge!( DictionaryService.get_records_for(letter) ) stash_result if i % 2 end end ## Data Partitioning Data is stored in RhoConnect using [redis sets](http://redis.io/commands#set). The `@result` hash from the `query` method is stored in redis and referred to as the Master Document or MD. The MD is referenced in RhoConnect by a corresponding partition. Source adapters can partition data in two ways: user and app. As you might have guessed, user partitioning stores a copy of the source adapter MD for each user (one copy shared across all devices for a user). Likewise, app partitioning stores one copy of the source adapter MD for the entire application (all users and devices share the same data). App partitioning can be particularly useful if you have source adapters which retrieve large amounts of data that is fixed from user to user, for example a global product catalog. Using app partitioning wherever possible ***greatly reduces*** the amount of data in redis. ### User Partition User partitioning is the default scheme for source adapters, however you can explicitly define it in `settings/settings.yml` with: :::yaml :sources: Product: :poll_interval: 300 :partition_type: user ### App Partition Enable app partitioning the same way: :::yaml :sources: Product: :poll_interval: 300 :partition_type: app Now you have a single copy of the `Product` source adapter dataset for all users. ## Pass Through RhoConnect provides a simple way to keep data out of redis. If you have sensitive data that you do not want saved in redis, add the pass_through option in settings/settings.yml for each source: :::yaml :sources: Product: :pass_through: true **NOTE: When running query or search the entire data set will be returned from your backend service. ** ## Store API RhoConnect provides a simple redis interface for saving/retrieving arbitrary data. This is useful if you want to save data in your application to be used later (i.e. in an async job or a subsequent source adapter execution). :::ruby Store.put_value('hello','world') Store.get_value('hello') #=> 'world' # You can store nested hashes too! Store.put_data( 'mydata', { '1' => { 'hello' => 'world' } } ) Store.get_data('mydata') #=> { '1' => { 'hello' => 'world' } } ## Handling Exceptions If your source adapter raises an instance of `SourceAdapterException`, the resulting message will be sent to the client's sync callback(in `@params['error_message']`). See the rhodes [sync exception handling docs](/rhodes/synchronization#handling-exceptions) for more details. You can use `SourceAdapterException` as a convenient way to notify your application of various error conditions. For example, your delete method might check the web service HTTP response code was 200 to make sure the record was deleted: :::ruby def delete(delete_hash) rest_result = RestClient.delete("#{@base}/#{delete_hash['id']}") if rest_result.code != 200 raise SourceAdapterException.new("Error deleting record.") end end **NOTE: When your adapter method raises an exception, no data is removed from the adapter's master document.** The following exceptions are provided for convenience: ### `SourceAdapterLoginException` Useful to raise in your adapter's login method if it failed. ### `SourceAdapterLogoffException` Similar to login, raise this if your adapter's logoff failed. ### `SourceAdapterServerTimeoutException` Raise if your backend service connection times out. ### `SourceAdapterServerErrorException` Raise this if your backend service returns a non-successful response. ## Handling Conflicts Handling conflicts in RhoConnect follows the same pattern as handling exceptions. Once your adapter method has detected a conflict, you can raise a `SourceAdapterObjectConflictError` which will be sent to your application's sync callback. ### `SourceAdapterObjectConflictError` Raise this if your adapter has detected a conflict. :::ruby def update(update_hash) obj_id = update_hash['id'] update_hash.delete('id') rest_result = RestClient.put("#{@base}/#{obj_id}",:product => update_hash) if rest_result.code != 200 raise SourceAdapterObjectConflictError.new("Conflict detected updating the object.") end end ## Sample Adapter Here's a complete example of how the completed [product adapter might look](https://github.com/rhomobile/store-server/blob/master/sources/product.rb): :::ruby require 'json' require 'rest_client' class Product < SourceAdapter def initialize(source) @base = 'http://rhostore.heroku.com/products' super(source) end def query(params=nil) rest_result = RestClient.get("#{@base}.json").body if rest_result.code != 200 raise SourceAdapterException.new("Error connecting!") end parsed = JSON.parse(rest_result) @result={} parsed.each do |item| @result[item["product"]["id"].to_s] = item["product"] end if parsed end def create(create_hash) res = RestClient.post(@base,:product => create_hash) # After create we are redirected to the new record. # We need to get the id of that record and return # it as part of create so rhoconnect can establish a link # from its temporary object on the client to this newly # created object on the server JSON.parse( RestClient.get("#{res.headers[:location]}.json").body )["product"]["id"] end def update(update_hash) obj_id = update_hash['id'] update_hash.delete('id') RestClient.put("#{@base}/#{obj_id}",:product => update_hash) end def delete(delete_hash) RestClient.delete("#{@base}/#{delete_hash['id']}") end end