cohabit 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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