cohabit 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +134 -11
- data/lib/cohabit.rb +12 -16
- data/lib/cohabit/configuration.rb +13 -2
- data/lib/cohabit/configuration/route_helper_scopes.rb +18 -0
- data/lib/cohabit/configuration/scopes.rb +3 -3
- data/lib/cohabit/configuration/settings.rb +17 -9
- data/lib/cohabit/configuration/strategies.rb +1 -1
- data/lib/cohabit/errors.rb +1 -0
- data/lib/cohabit/route_helper_scope.rb +65 -0
- data/lib/cohabit/scope.rb +25 -6
- data/lib/cohabit/strategies/basic.rb +4 -1
- data/lib/cohabit/strategies/defaults.rb +2 -1
- data/lib/cohabit/strategies/multi.rb +16 -13
- data/lib/cohabit/strategies/scope_validators.rb +20 -0
- data/lib/cohabit/strategy.rb +13 -1
- data/lib/cohabit/version.rb +1 -1
- data/test/scopes_test.rb +67 -5
- data/test/settings_test.rb +4 -11
- data/test/strategies_test.rb +10 -1
- data/test/test_helper.rb +1 -1
- metadata +16 -24
- data/lib/cohabit/snippets.rb +0 -4
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4ee059cff9113f9b438d138c88d31a229b7732b9
|
4
|
+
data.tar.gz: ade1a69aca01dc0f1c82ace515771a459053f9d1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ee9b05964894b01bd68cece139960de8234a73c3fbc673c4bb0a9ca0cee8add5c348135f804b846ed37aff1bc7c5671957e0369d36d7b0a7994a5cacecb81658
|
7
|
+
data.tar.gz: f83f0b91cc407b873384b396153720d48131586359949f49205c42c7a8ca231f9b4f1ead6203241658835eb412bdf32b10dca16b172ccf8296f7c64cdc72d20a
|
data/README.md
CHANGED
@@ -2,7 +2,9 @@
|
|
2
2
|
|
3
3
|
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).
|
4
4
|
|
5
|
-
|
5
|
+
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.
|
6
|
+
|
7
|
+
It provides (or it will):
|
6
8
|
|
7
9
|
- Model scoping (duh)
|
8
10
|
- Custom scoping strategies
|
@@ -11,15 +13,6 @@ It adds:
|
|
11
13
|
- Rake task for importing single-tenanted databases into a multi-tenant one
|
12
14
|
- Rake task for generating multi-tenanted scoped schema
|
13
15
|
|
14
|
-
## Todo
|
15
|
-
|
16
|
-
Still a WIP. Need to:
|
17
|
-
|
18
|
-
- Develop snippets to be included in strategies, i.e. scope_validations snippet (and remove that setting)
|
19
|
-
- Should snippets just be nested strategies? wah, probably.
|
20
|
-
- Work out how to integrate the url helper scopes as an option
|
21
|
-
- Write the rake tasks
|
22
|
-
|
23
16
|
## Installation
|
24
17
|
|
25
18
|
Add this line to your application's Gemfile:
|
@@ -36,7 +29,137 @@ Or install it yourself as:
|
|
36
29
|
|
37
30
|
## Usage
|
38
31
|
|
39
|
-
|
32
|
+
In its simplest form, using the basic scope (typical `belongs_to` assiciation scope):
|
33
|
+
|
34
|
+
# must have this line to use the included scopes
|
35
|
+
require 'basic'
|
36
|
+
scope [:foo, :bar], :basic
|
37
|
+
|
38
|
+
By default it assumes your tenant model is called tenant, if you wish to change this you can set it globally:
|
39
|
+
|
40
|
+
set :association, :organisation
|
41
|
+
scope [:foo, :bar], :basic
|
42
|
+
|
43
|
+
Or per scope with options:
|
44
|
+
|
45
|
+
scope [:foo, :bar], :basic, association: :organisation
|
46
|
+
|
47
|
+
Or you can specify options and other configuration settings in block form:
|
48
|
+
|
49
|
+
scope [:foo, :bar] do
|
50
|
+
use_strategy: :basic
|
51
|
+
set :association, :organisation
|
52
|
+
end
|
53
|
+
|
54
|
+
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:
|
55
|
+
|
56
|
+
class TenantSetup
|
57
|
+
def initialize(app)
|
58
|
+
@app = app
|
59
|
+
end
|
60
|
+
|
61
|
+
def call(env)
|
62
|
+
@request = Rack::Request.new(env)
|
63
|
+
Cohabit.current_tenant = Tenant.find_by_subdomain!(get_subdomain)
|
64
|
+
@app.call(env)
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
def get_subdomain
|
69
|
+
# Check request host isn't an IP.
|
70
|
+
host = @request.host
|
71
|
+
return nil unless !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
|
72
|
+
subdomain = host.split('.')[0..-3].first
|
73
|
+
return subdomain unless subdomain == "www"
|
74
|
+
return host.split('.')[0..-3][1]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
Alternatively, write a `before_filter` in the `ApplicationController`.
|
79
|
+
|
80
|
+
### Strategies
|
81
|
+
|
82
|
+
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:
|
83
|
+
|
84
|
+
strategy :basic do
|
85
|
+
# simply gets evaluated in the models..
|
86
|
+
model_eval do |_scope|
|
87
|
+
# _scope var references the scope that uses the strategy,
|
88
|
+
# so to access settings, like :association, use
|
89
|
+
# _scope.settings[:association]. current_tenant is defined
|
90
|
+
# as Cohabit.current_tenant.
|
91
|
+
|
92
|
+
# add relationship
|
93
|
+
belongs_to _scope.settings[:association]
|
94
|
+
|
95
|
+
# get foreign key
|
96
|
+
reflection = reflect_on_association _scope.settings[:association]
|
97
|
+
|
98
|
+
# scope insertions
|
99
|
+
before_create Proc.new {|m|
|
100
|
+
return unless Cohabit.current_tenant
|
101
|
+
m.send "#{_scope.settings[:association]}=".to_sym, Cohabit.current_tenant
|
102
|
+
}
|
103
|
+
|
104
|
+
# scope selects
|
105
|
+
default_scope lambda {
|
106
|
+
where(reflection.foreign_key => Cohabit.current_tenant) if Cohabit.current_tenant
|
107
|
+
}
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
You can define additional global vars in the Cohabit namespace, in your strategies, e.g.:
|
112
|
+
|
113
|
+
strategy :test do
|
114
|
+
set :globals, [:current_view, :current_scope]
|
115
|
+
# ...
|
116
|
+
end
|
117
|
+
|
118
|
+
# application_controller.rb
|
119
|
+
before_filter :set_scope
|
120
|
+
def set_scope
|
121
|
+
Cohabit.current_scope = Cohabit.current_tenant.managed_clients
|
122
|
+
end
|
123
|
+
|
124
|
+
You can also nest strategies to DRY up your code a bit.
|
125
|
+
|
126
|
+
strategy :basic_tweaked do
|
127
|
+
include_strategy :basic
|
128
|
+
model_eval do |_scope|
|
129
|
+
# ...
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
### Settings
|
134
|
+
|
135
|
+
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:
|
136
|
+
|
137
|
+
# routes file
|
138
|
+
# ...
|
139
|
+
resources :tenants do
|
140
|
+
resources :posts
|
141
|
+
resources :foo
|
142
|
+
resources :bar
|
143
|
+
end
|
144
|
+
# ...
|
145
|
+
|
146
|
+
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)`.
|
147
|
+
|
148
|
+
### Rake tasks
|
149
|
+
|
150
|
+
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:
|
151
|
+
|
152
|
+
1. Migrate DB or create new DB schema based on the scopes in a cohabit configuration file
|
153
|
+
2. Import a number of single-tenanted databases into the multi-tenanted equivalent
|
154
|
+
|
155
|
+
## Todo
|
156
|
+
|
157
|
+
Still a WIP. Need to:
|
158
|
+
|
159
|
+
- Work out how to integrate the url helper scopes as an option
|
160
|
+
- Write the rake tasks
|
161
|
+
- Add custom `cohabit_unscoped` (or similar) class method to models which removes all Cohabit `default_scope`s, `before_create`s, validation scopes, etc for that chain. (Possible? hmf)
|
162
|
+
- Add a `conditions` option to `include_strategy`, like that of Rails routes perhaps
|
40
163
|
|
41
164
|
## Contributing
|
42
165
|
|
data/lib/cohabit.rb
CHANGED
@@ -2,34 +2,30 @@ require 'cohabit/errors'
|
|
2
2
|
require 'cohabit/configuration'
|
3
3
|
require 'cohabit/strategy'
|
4
4
|
require 'cohabit/scope'
|
5
|
+
require 'cohabit/route_helper_scope'
|
5
6
|
|
6
7
|
module Cohabit
|
8
|
+
@data = {}
|
7
9
|
class << self
|
8
10
|
attr_accessor :current_tenant
|
9
|
-
end
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
instance_exec(_scope, &proc) if proc
|
12
|
+
def add_global(name)
|
13
|
+
singleton_class.send(:define_method, name) { @data[name] }
|
14
|
+
singleton_class.send(:define_method, "#{name}=") { |val| @data[name] = val }
|
15
15
|
end
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
if defined? ::Rails::Railtie
|
19
|
+
if defined?(Rails) && Rails::VERSION::MAJOR.to_i >= 3
|
22
20
|
module Cohabit
|
23
21
|
class Railtie < Rails::Railtie
|
24
|
-
initializer "cohabit.
|
25
|
-
|
26
|
-
|
27
|
-
|
22
|
+
initializer "cohabit.initialize_scopes" do
|
23
|
+
ActiveSupport.on_load :after_initialize do
|
24
|
+
config = Cohabit::Configuration.new
|
25
|
+
config.load(file: File.join(Rails.root, "config/cohabit.rb"))
|
26
|
+
config.apply_all!
|
27
|
+
end
|
28
28
|
end
|
29
29
|
end
|
30
30
|
end
|
31
|
-
else
|
32
|
-
config = Cohabit::Configuration.new
|
33
|
-
config.load(file: File.join(Rails.root, "config/cohabit.rb"))
|
34
|
-
config.apply_scopes!
|
35
31
|
end
|
@@ -2,7 +2,9 @@ require "cohabit/version"
|
|
2
2
|
require "cohabit/configuration/settings"
|
3
3
|
require "cohabit/configuration/strategies"
|
4
4
|
require "cohabit/configuration/scopes"
|
5
|
+
require "cohabit/configuration/route_helper_scopes"
|
5
6
|
require "active_record"
|
7
|
+
require "active_support/inflector"
|
6
8
|
|
7
9
|
module Cohabit
|
8
10
|
class Configuration
|
@@ -10,7 +12,7 @@ module Cohabit
|
|
10
12
|
attr_reader :load_paths
|
11
13
|
|
12
14
|
def initialize(config_file = nil)
|
13
|
-
@load_paths = [".", File.expand_path(File.join(File.dirname(__FILE__), "strategies"))]
|
15
|
+
@load_paths = [Rails.root.join("lib"), Rails.root.join("config"), ".", File.expand_path(File.join(File.dirname(__FILE__), "strategies"))]
|
14
16
|
end
|
15
17
|
|
16
18
|
def load(*args, &block)
|
@@ -40,7 +42,16 @@ module Cohabit
|
|
40
42
|
end
|
41
43
|
end
|
42
44
|
|
43
|
-
|
45
|
+
def apply_all!
|
46
|
+
self.class.ancestors.take_while{|a| a != self.class.superclass}
|
47
|
+
.reject{|a| a == self.class}
|
48
|
+
.each do |a|
|
49
|
+
method = "apply_#{a.name.demodulize.underscore}!"
|
50
|
+
self.send(method, self) if self.respond_to?(method)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
include Settings, Strategies, Scopes, RouteHelperScopes
|
44
55
|
|
45
56
|
end
|
46
57
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Cohabit
|
2
|
+
class Configuration
|
3
|
+
module RouteHelperScopes
|
4
|
+
|
5
|
+
attr_accessor :route_scopes
|
6
|
+
|
7
|
+
def scope_route_helpers(*args)
|
8
|
+
generate_settings_hash!(args)
|
9
|
+
(@route_scopes ||= []) << RouteHelperScope.new(*args)
|
10
|
+
end
|
11
|
+
|
12
|
+
def apply_route_helper_scopes!(context = self)
|
13
|
+
@route_scopes.each{|rs| rs.apply!(context)}
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -16,13 +16,13 @@ module Cohabit
|
|
16
16
|
attr_reader :scopes
|
17
17
|
|
18
18
|
def scope(*args, &block)
|
19
|
-
|
19
|
+
generate_settings_hash!(args)
|
20
20
|
scope = Scope.new(*args, &block)
|
21
21
|
add_scope(scope)
|
22
22
|
end
|
23
23
|
|
24
|
-
def apply_scopes!
|
25
|
-
@scopes.each{ |s| s.apply! }
|
24
|
+
def apply_scopes!(context = self)
|
25
|
+
@scopes.each{ |s| s.apply!(context) }
|
26
26
|
end
|
27
27
|
|
28
28
|
private
|
@@ -2,12 +2,13 @@ module Cohabit
|
|
2
2
|
class Configuration
|
3
3
|
module Settings
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
association: :tenant
|
5
|
+
DEFAULT_SETTINGS = {
|
6
|
+
association: :tenant,
|
7
|
+
association_as: :tenant
|
9
8
|
}
|
10
9
|
|
10
|
+
CUSTOM_HANDLERS = [:globals]
|
11
|
+
|
11
12
|
def self.included(base)
|
12
13
|
base.send :alias_method, :initialize_without_settings, :initialize
|
13
14
|
base.send :alias_method, :initialize, :initialize_with_settings
|
@@ -17,12 +18,12 @@ module Cohabit
|
|
17
18
|
attr_reader :settings
|
18
19
|
|
19
20
|
def initialize_with_settings(*args, &block)
|
20
|
-
@settings =
|
21
|
+
@settings = DEFAULT_SETTINGS.dup
|
21
22
|
initialize_without_settings(*args, &block)
|
22
23
|
end
|
23
24
|
|
24
25
|
def merge_settings!(settings)
|
25
|
-
settings.delete_if{ |s| !
|
26
|
+
settings.delete_if{ |s| !DEFAULT_SETTINGS.include?(s) }
|
26
27
|
@settings.merge!(settings)
|
27
28
|
end
|
28
29
|
|
@@ -35,10 +36,17 @@ module Cohabit
|
|
35
36
|
end
|
36
37
|
|
37
38
|
def set(setting, value)
|
38
|
-
|
39
|
-
|
39
|
+
setting = setting.to_sym
|
40
|
+
if CUSTOM_HANDLERS.include?(setting)
|
41
|
+
send("set_#{setting}", value) and return
|
42
|
+
end
|
43
|
+
@settings[setting] = value
|
44
|
+
end
|
45
|
+
|
46
|
+
def set_globals(value)
|
47
|
+
[value].flatten.each do |v|
|
48
|
+
Cohabit.add_global(v) unless Cohabit.respond_to?(v)
|
40
49
|
end
|
41
|
-
@settings[setting.to_sym] = value
|
42
50
|
end
|
43
51
|
|
44
52
|
end
|
data/lib/cohabit/errors.rb
CHANGED
@@ -0,0 +1,65 @@
|
|
1
|
+
module Cohabit
|
2
|
+
class RouteHelperScope
|
3
|
+
|
4
|
+
def initialize(*args)
|
5
|
+
merge_settings!(args.last) if args.last.is_a?(Hash)
|
6
|
+
end
|
7
|
+
|
8
|
+
include Configuration::Settings
|
9
|
+
|
10
|
+
def apply!(context)
|
11
|
+
named_routes = get_route_helpers
|
12
|
+
|
13
|
+
route_helpers = Module.new
|
14
|
+
route_helpers.module_exec(named_routes, @settings[:association], &route_override_proc)
|
15
|
+
Cohabit.const_set("RouteHelpers", route_helpers)
|
16
|
+
ActionView::Base.send :include, Cohabit::RouteHelpers
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
def get_route_helpers
|
21
|
+
named_routes_arr = Rails.application.routes.named_routes
|
22
|
+
.find_all{|rn, _| rn =~ /#{Regexp.escape(@settings[:association_as])}_/}
|
23
|
+
.group_by do |_, r|
|
24
|
+
name = r.path.names[r.path.names.index("#{@settings[:association_as]}_id")+1]
|
25
|
+
if name =~ /_id/
|
26
|
+
name.gsub("_id", "")
|
27
|
+
else
|
28
|
+
r.defaults[:controller].classify.downcase
|
29
|
+
end
|
30
|
+
end
|
31
|
+
.reject{|k, _| k.nil?}
|
32
|
+
.map{|g, rs| [g, Hash[rs.map{|rn, r| [rn.to_s.gsub("#{@settings[:association_as]}_", "").to_sym, rn]}]]}
|
33
|
+
# e.g. { student: { :school_student => :student, :school_edit_student => :edit_student } }
|
34
|
+
return Hash[named_routes_arr]
|
35
|
+
end
|
36
|
+
|
37
|
+
def route_override_proc
|
38
|
+
Proc.new do |named_routes, assoc|
|
39
|
+
named_routes.each do |mr, rs|
|
40
|
+
rs.each do |r, orig_r|
|
41
|
+
module_eval <<-EOT, __FILE__, __LINE__ + 1
|
42
|
+
def #{r}_path(*args)
|
43
|
+
do_dat_thang!("path", "#{assoc}", "#{mr}", "#{orig_r}", *args) || super
|
44
|
+
end
|
45
|
+
def #{r}_url(*args)
|
46
|
+
do_dat_thang!("url", "#{assoc}", "#{mr}", "#{orig_r}", *args) || super
|
47
|
+
end
|
48
|
+
EOT
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def do_dat_thang!(type, assoc, main_resource, orig_route, obj, *args)
|
53
|
+
if obj.is_a?(Integer)
|
54
|
+
model = main_resource.classify.constantize
|
55
|
+
obj = model.find(obj)
|
56
|
+
end
|
57
|
+
if Cohabit.current_tenant.is_cluster? && obj.respond_to?(assoc)
|
58
|
+
send("#{orig_route}_#{type}", obj.send(assoc), obj, *args)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
data/lib/cohabit/scope.rb
CHANGED
@@ -10,7 +10,6 @@ module Cohabit
|
|
10
10
|
apply_to(args[0])
|
11
11
|
use_strategy(args[1])
|
12
12
|
instance_eval(&block) unless block.nil?
|
13
|
-
merge_settings!(@strategy.settings)
|
14
13
|
unless valid?
|
15
14
|
raise InvalidScopeError, "provide valid model(s) and strategy"
|
16
15
|
end
|
@@ -22,7 +21,7 @@ module Cohabit
|
|
22
21
|
|
23
22
|
def use_strategy(strategy)
|
24
23
|
unless strategy.nil?
|
25
|
-
@
|
24
|
+
@strategy_name = strategy
|
26
25
|
end
|
27
26
|
end
|
28
27
|
|
@@ -30,17 +29,22 @@ module Cohabit
|
|
30
29
|
@models = parse_models(models)
|
31
30
|
end
|
32
31
|
|
33
|
-
def apply!
|
34
|
-
# apply yourself
|
32
|
+
def apply!(context)
|
33
|
+
# apply yourself! that's what my teachers always said.
|
34
|
+
strategy_stack = get_strategies(@strategy_name, context)
|
35
|
+
main_strategy = context.find_strategy_by_name(@strategy_name)
|
36
|
+
merge_settings!(main_strategy.settings)
|
35
37
|
@models.each do |model|
|
36
|
-
|
38
|
+
strategy_stack.each do |strategy|
|
39
|
+
model.instance_exec(self, &strategy.model_code)
|
40
|
+
end
|
37
41
|
end
|
38
42
|
end
|
39
43
|
|
40
44
|
private
|
41
45
|
def valid?
|
42
46
|
return false if @models.empty?
|
43
|
-
return false if @
|
47
|
+
return false if @strategy_name.nil?
|
44
48
|
@models.each do |model|
|
45
49
|
return false if !ActiveRecord::Base.descendants.include?(model)
|
46
50
|
end
|
@@ -53,5 +57,20 @@ module Cohabit
|
|
53
57
|
end
|
54
58
|
end
|
55
59
|
|
60
|
+
def get_strategies(strategy_name, context, strategy_stack = [])
|
61
|
+
strategy = context.find_strategy_by_name(strategy_name)
|
62
|
+
strategy.strategies.each do |s|
|
63
|
+
if s == strategy.name
|
64
|
+
if strategy_stack.include?(strategy)
|
65
|
+
raise StrategyNestingError, "strategies can't be nested twice in the same stack"
|
66
|
+
end
|
67
|
+
strategy_stack << strategy
|
68
|
+
else
|
69
|
+
strategy_stack = get_strategies(s, context, strategy_stack)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
strategy_stack
|
73
|
+
end
|
74
|
+
|
56
75
|
end
|
57
76
|
end
|
@@ -1,8 +1,11 @@
|
|
1
1
|
strategy :basic do
|
2
2
|
model_eval do |_scope|
|
3
|
+
# _scope var references the scope that uses the strategy,
|
4
|
+
# so to access settings, like :association, use
|
5
|
+
# _scope.settings[:association]. current_tenant is defined
|
6
|
+
# as Cohabit.current_tenant.
|
3
7
|
belongs_to _scope.settings[:association]
|
4
8
|
reflection = reflect_on_association _scope.settings[:association]
|
5
|
-
scope_validators(reflection.foreign_key) if _scope.settings[:scope_validations]
|
6
9
|
before_create Proc.new {|m|
|
7
10
|
return unless Cohabit.current_tenant
|
8
11
|
m.send "#{_scope.settings[:association]}=".to_sym, Cohabit.current_tenant
|
@@ -1,13 +1,16 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
#
|
7
|
-
|
8
|
-
|
9
|
-
#
|
10
|
-
|
11
|
-
#
|
12
|
-
|
13
|
-
|
1
|
+
strategy :multi do
|
2
|
+
set :globals, :current_scope
|
3
|
+
model_eval do |_scope|
|
4
|
+
belongs_to _scope.settings[:association]
|
5
|
+
reflection = reflect_on_association _scope.settings[:association]
|
6
|
+
# insertions are scoped to current_tenant
|
7
|
+
before_create Proc.new {|m|
|
8
|
+
return unless Cohabit.current_tenant
|
9
|
+
m.send "#{association}=".to_sym, Cohabit.current_tenant
|
10
|
+
}
|
11
|
+
# selects are scoped to multiple clients (stored in current_scope)
|
12
|
+
default_scope lambda {
|
13
|
+
where(reflection.foreign_key => Cohabit.current_scope) if Cohabit.current_scope
|
14
|
+
}
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
strategy :scope_validators do
|
2
|
+
model_eval do |_scope|
|
3
|
+
reflection = reflect_on_association _scope.settings[:association]
|
4
|
+
foreign_key = reflection.foreign_key
|
5
|
+
_validators.each do |attribute, validations|
|
6
|
+
validations.reject!{|v| v.kind == :uniqueness}
|
7
|
+
end
|
8
|
+
new_callback_chain = self._validate_callbacks.reject do |callback|
|
9
|
+
callback.raw_filter.is_a?(ActiveRecord::Validations::UniquenessValidator)
|
10
|
+
end
|
11
|
+
deleted = self._validate_callbacks - new_callback_chain
|
12
|
+
(self._validate_callbacks.clear << new_callback_chain).flatten!
|
13
|
+
deleted.each do |c|
|
14
|
+
v = c.raw_filter
|
15
|
+
v.attributes.each do |a|
|
16
|
+
validates_uniqueness_of *v.attributes, v.options.merge(scope: foreign_key)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/cohabit/strategy.rb
CHANGED
@@ -5,6 +5,7 @@ module Cohabit
|
|
5
5
|
|
6
6
|
def initialize(*args, &block)
|
7
7
|
raise ArgumentError, "you must supply a name" if args.empty?
|
8
|
+
@strategies = []
|
8
9
|
@name = args.shift.to_sym
|
9
10
|
@settings = args.last.is_a?(Hash) ? args.last : {}
|
10
11
|
instance_eval(&block) unless block.nil?
|
@@ -12,10 +13,21 @@ module Cohabit
|
|
12
13
|
|
13
14
|
include Configuration::Settings
|
14
15
|
|
15
|
-
attr_reader :name, :model_code
|
16
|
+
attr_reader :name, :model_code, :strategies
|
16
17
|
|
17
18
|
def model_eval(&block)
|
18
19
|
@model_code = block
|
20
|
+
@strategies << @name
|
21
|
+
end
|
22
|
+
|
23
|
+
def include_strategy(name, options = {})
|
24
|
+
name = name.to_sym
|
25
|
+
raise ArgumentError if name.nil?
|
26
|
+
if @strategies.include?(name)
|
27
|
+
raise Argumenterror, "can't nest the same strategy twice
|
28
|
+
or use two model_eval blocks in the same strategy"
|
29
|
+
end
|
30
|
+
@strategies << name
|
19
31
|
end
|
20
32
|
|
21
33
|
end
|
data/lib/cohabit/version.rb
CHANGED
data/test/scopes_test.rb
CHANGED
@@ -31,11 +31,20 @@ class ScopesTest < Test::Unit::TestCase
|
|
31
31
|
scope :ybur, :basic, association: :client
|
32
32
|
end
|
33
33
|
assert_equal(:client, @c.scopes.first.settings[:association])
|
34
|
-
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_settings_for_scope_globally
|
37
|
+
@c.load do
|
38
|
+
require 'basic'
|
39
|
+
set :association, :client
|
40
|
+
scope :ybur, :basic
|
41
|
+
end
|
42
|
+
assert_equal(:client, @c.scopes.first.settings[:association])
|
43
|
+
end
|
35
44
|
|
36
45
|
def test_apply_scope_to_model
|
37
|
-
|
38
|
-
Cohabit.current_tenant =
|
46
|
+
client = Client.create(name: "fubar")
|
47
|
+
Cohabit.current_tenant = client
|
39
48
|
@c.load do
|
40
49
|
require 'basic'
|
41
50
|
scope :ybur, :basic
|
@@ -45,8 +54,8 @@ class ScopesTest < Test::Unit::TestCase
|
|
45
54
|
end
|
46
55
|
|
47
56
|
def test_setting_association_name
|
48
|
-
|
49
|
-
Cohabit.current_tenant =
|
57
|
+
client = Client.create(name: "fubar")
|
58
|
+
Cohabit.current_tenant = client
|
50
59
|
@c.load do
|
51
60
|
require 'basic'
|
52
61
|
scope :ybur, :basic, association: :client
|
@@ -55,4 +64,57 @@ class ScopesTest < Test::Unit::TestCase
|
|
55
64
|
assert_match(/client_id/, Ybur.scoped.to_sql)
|
56
65
|
end
|
57
66
|
|
67
|
+
def test_nested_strategy_scope_basic
|
68
|
+
client = Client.create(name: "fubar")
|
69
|
+
Cohabit.current_tenant = client
|
70
|
+
# should run like normal basic strategy
|
71
|
+
@c.load do
|
72
|
+
require 'basic'
|
73
|
+
strategy :frankel, { association: :client } do
|
74
|
+
include_strategy :basic
|
75
|
+
end
|
76
|
+
scope :ybur, :frankel
|
77
|
+
end
|
78
|
+
@c.apply_scopes!
|
79
|
+
assert_not_equal(Ybur.unscoped.to_sql, Ybur.scoped.to_sql)
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_nested_strategy_scope_override
|
83
|
+
client = Client.create(name: "fubar")
|
84
|
+
Cohabit.current_tenant = client
|
85
|
+
# should remove basic strategy's default scope, as it
|
86
|
+
# is evaluated after it in the main strategy
|
87
|
+
@c.load do
|
88
|
+
require 'basic'
|
89
|
+
strategy :frankel, { association: :client } do
|
90
|
+
include_strategy :basic
|
91
|
+
model_eval do |_scope|
|
92
|
+
default_scopes.clear
|
93
|
+
end
|
94
|
+
end
|
95
|
+
scope :ybur, :frankel
|
96
|
+
end
|
97
|
+
@c.apply_scopes!
|
98
|
+
assert_equal(Ybur.unscoped.to_sql, Ybur.scoped.to_sql)
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_evaluation_order_in_nested_strategies
|
102
|
+
client = Client.create(name: "fubar")
|
103
|
+
Cohabit.current_tenant = client
|
104
|
+
# should remove basic strategy's default scope, as it
|
105
|
+
# is evaluated after it in the main strategy
|
106
|
+
@c.load do
|
107
|
+
require 'basic'
|
108
|
+
strategy :frankel, { association: :client } do
|
109
|
+
model_eval do |_scope|
|
110
|
+
default_scopes.clear
|
111
|
+
end
|
112
|
+
include_strategy :basic
|
113
|
+
end
|
114
|
+
scope :ybur, :frankel
|
115
|
+
end
|
116
|
+
@c.apply_scopes!
|
117
|
+
assert_not_equal(Ybur.unscoped.to_sql, Ybur.scoped.to_sql)
|
118
|
+
end
|
119
|
+
|
58
120
|
end
|
data/test/settings_test.rb
CHANGED
@@ -4,24 +4,17 @@ class SettingsTest < Test::Unit::TestCase
|
|
4
4
|
@c = Cohabit::Configuration.new
|
5
5
|
end
|
6
6
|
|
7
|
-
def
|
8
|
-
assert_not_equal(@c.settings[:scope_validations], true)
|
7
|
+
def test_set_setting
|
9
8
|
@c.set :scope_validations, true
|
10
9
|
assert_equal(true, @c.settings[:scope_validations])
|
11
10
|
end
|
12
11
|
|
13
|
-
def test_set_nonexitant_setting
|
14
|
-
assert_raise(ArgumentError) do
|
15
|
-
@c.set :wtf_not_a_real_setting, "dis is a value, innit"
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
12
|
def test_set_setting_with_config
|
20
|
-
assert_equal(
|
13
|
+
assert_equal(:tenant, @c.settings[:association])
|
21
14
|
@c.load do
|
22
|
-
set :
|
15
|
+
set :association, :client
|
23
16
|
end
|
24
|
-
assert_equal(
|
17
|
+
assert_equal(:client, @c.settings[:association])
|
25
18
|
end
|
26
19
|
|
27
20
|
end
|
data/test/strategies_test.rb
CHANGED
@@ -37,7 +37,6 @@ class StrategiesTest < Test::Unit::TestCase
|
|
37
37
|
# strategy block defaults > strategy arg defaults
|
38
38
|
|
39
39
|
# should take on strategy default settings when passed in
|
40
|
-
setup
|
41
40
|
@c.load do
|
42
41
|
strategy :frankel, { scope_validations: true }
|
43
42
|
end
|
@@ -54,4 +53,14 @@ class StrategiesTest < Test::Unit::TestCase
|
|
54
53
|
assert_equal(true, @c.strategies.first.settings[:scope_validations])
|
55
54
|
end
|
56
55
|
|
56
|
+
def test_nested_strategy
|
57
|
+
@c.load do
|
58
|
+
strategy :jim
|
59
|
+
strategy :frankel, { association: :client } do
|
60
|
+
include_strategy :jim
|
61
|
+
end
|
62
|
+
end
|
63
|
+
assert_equal([:jim], @c.find_strategy_by_name(:frankel).strategies)
|
64
|
+
end
|
65
|
+
|
57
66
|
end
|
data/test/test_helper.rb
CHANGED
@@ -3,9 +3,9 @@ require "test/unit"
|
|
3
3
|
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
4
4
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
5
5
|
|
6
|
+
require "cohabit"
|
6
7
|
require "active_record"
|
7
8
|
require "models"
|
8
|
-
require "cohabit"
|
9
9
|
|
10
10
|
def load_schema
|
11
11
|
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
|
metadata
CHANGED
@@ -1,52 +1,46 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cohabit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
5
|
-
prerelease:
|
4
|
+
version: 0.0.2
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Mike Campbell
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date:
|
11
|
+
date: 2014-03-18 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: activerecord
|
16
15
|
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
16
|
requirements:
|
19
|
-
- -
|
17
|
+
- - '>='
|
20
18
|
- !ruby/object:Gem::Version
|
21
19
|
version: '0'
|
22
20
|
type: :runtime
|
23
21
|
prerelease: false
|
24
22
|
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
23
|
requirements:
|
27
|
-
- -
|
24
|
+
- - '>='
|
28
25
|
- !ruby/object:Gem::Version
|
29
26
|
version: '0'
|
30
27
|
- !ruby/object:Gem::Dependency
|
31
28
|
name: activesupport
|
32
29
|
requirement: !ruby/object:Gem::Requirement
|
33
|
-
none: false
|
34
30
|
requirements:
|
35
|
-
- -
|
31
|
+
- - '>='
|
36
32
|
- !ruby/object:Gem::Version
|
37
33
|
version: '0'
|
38
34
|
type: :runtime
|
39
35
|
prerelease: false
|
40
36
|
version_requirements: !ruby/object:Gem::Requirement
|
41
|
-
none: false
|
42
37
|
requirements:
|
43
|
-
- -
|
38
|
+
- - '>='
|
44
39
|
- !ruby/object:Gem::Version
|
45
40
|
version: '0'
|
46
41
|
- !ruby/object:Gem::Dependency
|
47
42
|
name: bundler
|
48
43
|
requirement: !ruby/object:Gem::Requirement
|
49
|
-
none: false
|
50
44
|
requirements:
|
51
45
|
- - ~>
|
52
46
|
- !ruby/object:Gem::Version
|
@@ -54,7 +48,6 @@ dependencies:
|
|
54
48
|
type: :development
|
55
49
|
prerelease: false
|
56
50
|
version_requirements: !ruby/object:Gem::Requirement
|
57
|
-
none: false
|
58
51
|
requirements:
|
59
52
|
- - ~>
|
60
53
|
- !ruby/object:Gem::Version
|
@@ -62,17 +55,15 @@ dependencies:
|
|
62
55
|
- !ruby/object:Gem::Dependency
|
63
56
|
name: rake
|
64
57
|
requirement: !ruby/object:Gem::Requirement
|
65
|
-
none: false
|
66
58
|
requirements:
|
67
|
-
- -
|
59
|
+
- - '>='
|
68
60
|
- !ruby/object:Gem::Version
|
69
61
|
version: '0'
|
70
62
|
type: :development
|
71
63
|
prerelease: false
|
72
64
|
version_requirements: !ruby/object:Gem::Requirement
|
73
|
-
none: false
|
74
65
|
requirements:
|
75
|
-
- -
|
66
|
+
- - '>='
|
76
67
|
- !ruby/object:Gem::Version
|
77
68
|
version: '0'
|
78
69
|
description: Handle application scoping for multi-tenant applications with table scopes.
|
@@ -90,15 +81,17 @@ files:
|
|
90
81
|
- cohabit.gemspec
|
91
82
|
- lib/cohabit.rb
|
92
83
|
- lib/cohabit/configuration.rb
|
84
|
+
- lib/cohabit/configuration/route_helper_scopes.rb
|
93
85
|
- lib/cohabit/configuration/scopes.rb
|
94
86
|
- lib/cohabit/configuration/settings.rb
|
95
87
|
- lib/cohabit/configuration/strategies.rb
|
96
88
|
- lib/cohabit/errors.rb
|
89
|
+
- lib/cohabit/route_helper_scope.rb
|
97
90
|
- lib/cohabit/scope.rb
|
98
|
-
- lib/cohabit/snippets.rb
|
99
91
|
- lib/cohabit/strategies/basic.rb
|
100
92
|
- lib/cohabit/strategies/defaults.rb
|
101
93
|
- lib/cohabit/strategies/multi.rb
|
94
|
+
- lib/cohabit/strategies/scope_validators.rb
|
102
95
|
- lib/cohabit/strategy.rb
|
103
96
|
- lib/cohabit/version.rb
|
104
97
|
- test/all_tests.rb
|
@@ -113,27 +106,26 @@ files:
|
|
113
106
|
homepage: http://github.com/mikecmpbll/cohabit
|
114
107
|
licenses:
|
115
108
|
- MIT
|
109
|
+
metadata: {}
|
116
110
|
post_install_message:
|
117
111
|
rdoc_options: []
|
118
112
|
require_paths:
|
119
113
|
- lib
|
120
114
|
required_ruby_version: !ruby/object:Gem::Requirement
|
121
|
-
none: false
|
122
115
|
requirements:
|
123
|
-
- -
|
116
|
+
- - '>='
|
124
117
|
- !ruby/object:Gem::Version
|
125
118
|
version: '0'
|
126
119
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
|
-
none: false
|
128
120
|
requirements:
|
129
|
-
- -
|
121
|
+
- - '>='
|
130
122
|
- !ruby/object:Gem::Version
|
131
123
|
version: '0'
|
132
124
|
requirements: []
|
133
125
|
rubyforge_project:
|
134
|
-
rubygems_version:
|
126
|
+
rubygems_version: 2.0.6
|
135
127
|
signing_key:
|
136
|
-
specification_version:
|
128
|
+
specification_version: 4
|
137
129
|
summary: Scope multi-tenant applications.
|
138
130
|
test_files:
|
139
131
|
- test/all_tests.rb
|
data/lib/cohabit/snippets.rb
DELETED