Connecting to a backend service with RhoSync 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”.
Adding a new source adapter to your RhoSync application is simple:
$ cd storeserver $ rhosync source product
This will generate a new ruby class called sources/product.rb
:
class Product < SourceAdapter def initialize(source,credential) super(source,credential) 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,blob=nil) # 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(object_id) # 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 hash values in name_value_list" 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:
:sources: Product: :poll_interval: 300
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).
def login MyWebService.login(current_user.login) end
logoff
Logoff from your backend service (optional).
def logoff MyWebService.logoff(current_user.login) end
query(params = nil)
Query your backend service and build a hash of hashes (required).
This method must assign
@result
to a hash of hashes. If @result is nil
or {}
, the master document will be erased from redis.
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.
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.
create(create_hash)
Create a new record in the backend (optional).
RhoSync 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.
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).
def update(update_hash) end
delete(delete_hash)
Delete an existing record in the backend (optional).
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:
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.
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 is stored in RhoSync using redis sets. 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 RhoSync 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 partitioning is the default scheme for source adapters, however you can explicitly define it in settings/settings.yml
with:
:sources: Product: :poll_interval: 300 :partition_type: user
Enable app partitioning the same way:
:sources: Product: :poll_interval: 300 :partition_type: app
Now you have a single copy of the Product
source adapter dataset for all users.
RhoSync 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).
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' } }
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 for more details.
For example, your delete method might check the web service HTTP response code was 200 to make sure the record was deleted:
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
When your adapter method raises an exception, no data is removed from the adapter’s master document.
Here’s a complete example of how the completed product adapter might look:
require 'json' require 'rest_client' class Product < SourceAdapter def initialize(source,credential) @base = 'http://rhostore.heroku.com/products' super(source,credential) 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 rhosync 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