Cohabit
Cohabit adds comprehensive scoped multi-tenancy functionality to any application, simply set your options up in config/cohabit.rb using the DSL (inspired by capistrano).
This gem isn't really recommended for doing simple application wide scoping, for that I'd recommend https://github.com/wireframe/multitenant. Cohabit builds on what multitenant provides and allows you to define your own scoping strategies with the DSL for where more complexity is needed.
It provides (or it will):
- Model scoping (duh)
- Custom scoping strategies
- Scope validations
- Scope URL helpers
- Rake task for importing single-tenanted databases into a multi-tenant one
- Rake task for generating multi-tenanted scoped schema
Installation
Add this line to your application's Gemfile:
gem 'cohabit'
And then execute:
$ bundle
Or install it yourself as:
$ gem install cohabit
Usage
In its simplest form, using the basic scope (typical belongs_to assiciation scope):
# must have this line to use the included scopes
require 'basic'
scope [:foo, :bar], :basic
By default it assumes your tenant model is called tenant, if you wish to change this you can set it globally:
set :association, :organisation
scope [:foo, :bar], :basic
Or per scope with options:
scope [:foo, :bar], :basic, association: :organisation
Or you can specify options and other configuration settings in block form:
scope [:foo, :bar] do
  use_strategy: :basic
  set :association, :organisation
end
In your application, depending on how you determine the current tenant, you need to set Cohabit.current_tenant. If you're using subdomains, I would recommend writing some simple Rack middleware something like:
class TenantSetup
  def initialize(app)
    @app = app
  end
  def call(env)
    @request = Rack::Request.new(env)
    Cohabit.current_tenant = Tenant.find_by_subdomain!(get_subdomain)
    @app.call(env)
  end
  private
    def get_subdomain
      # Check request host isn't an IP.
      host = @request.host
      return nil unless !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
      subdomain = host.split('.')[0..-3].first
      return subdomain unless subdomain == "www"
      return host.split('.')[0..-3][1]
    end
end
Alternatively, write a before_filter in the ApplicationController.
Strategies
This part is a WIP, but you can define your own strategies to be used. The following is the :basic strategy that comes by default:
strategy :basic do
  # simply gets evaluated in the models..
  model_eval do |_scope|
    # _scope var references the scope that uses the strategy,
    # so to access settings, like :association, use
    # _scope.settings[:association]. current_tenant is defined
    # as Cohabit.current_tenant.
    # add relationship
    belongs_to _scope.settings[:association]
    # get foreign key
    reflection = reflect_on_association _scope.settings[:association]
    # scope insertions
    before_create Proc.new {|m|
      return unless Cohabit.current_tenant
      m.send "#{_scope.settings[:association]}=".to_sym, Cohabit.current_tenant
    }
    # scope selects
    default_scope lambda {
      where(reflection.foreign_key => Cohabit.current_tenant) if Cohabit.current_tenant
    }
  end
end
You can define additional global vars in the Cohabit namespace, in your strategies, e.g.:
strategy :test do
  set :globals, [:current_view, :current_scope]
  # ...
end
# application_controller.rb
before_filter :set_scope
def set_scope
  Cohabit.current_scope = Cohabit.current_tenant.managed_clients
end
You can also nest strategies to DRY up your code a bit.
strategy :basic_tweaked do
  include_strategy :basic
  model_eval do |_scope|
    # ...
  end
end
Settings
Once I've implemented it, you'll be able to scope URL helpers, so for example if your tenant features in your URL like so:
# routes file
# ...
resources :tenants do
  resources :posts
  resources :foo
  resources :bar
end
# ...
Giving you the paths /tenant/1/posts/1 .. etc. You will be able to still call posts_path(@post), and when the setting is enabled it will expand that internally to tenants_posts_path(@post.tenant, @post).
Rake tasks
There are two rake tasks in the pipeline to make life a bit easier for anyone converting from multi-database architecture to a multi-tenant, single-database architecture:
- Migrate DB or create new DB schema based on the scopes in a cohabit configuration file
- Import a number of single-tenanted databases into the multi-tenanted equivalent
Todo
Still a WIP. Need to:
- Work out how to integrate the url helper scopes as an option
- Write the rake tasks
- Add custom cohabit_unscoped(or similar) class method to models which removes all Cohabitdefault_scopes,before_creates, validation scopes, etc for that chain. (Possible? hmf)
- Add a conditionsoption toinclude_strategy, like that of Rails routes perhaps
Contributing
- Fork it
- Create your feature branch (git checkout -b my-new-feature)
- Commit your changes (git commit -am 'Add some feature')
- Push to the branch (git push origin my-new-feature)
- Create new Pull Request