mongoid-multitenancy 0.4.4 → 1.0.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7a20d8d26c776318f1eb6407d0dc19017a721cdb
4
- data.tar.gz: 1404d16b8b568ebf9a7744ce7f6fa5c5338fa91b
3
+ metadata.gz: 95438e3e7adc7dc21d06a0deec16979183b37738
4
+ data.tar.gz: 61ce79abafb106d53af8dbf35c74523a04b43a31
5
5
  SHA512:
6
- metadata.gz: bed65acf48172bfaac61012ebdb5bc67bb73e96500835273e7ef45743cc7e77bddd5a32ed4b2ed52f11441e735f484311d1a192047a2b594c11b07e4ba4e1dce
7
- data.tar.gz: 1271fa93a141b513d1d5587b6d4bd7ce4c0b294635d243292f35cc62cfe60349abe78b499ba689bc7913868746cb1ee6a91400d4a5ecf7bb0e1a691bf196db5c
6
+ metadata.gz: 58cf2892dd86cd822f1fc436e745e53ffd128680164226323e5e32ac328bd14db877cd990ea29e01db130e4155b205a1de1a9b6cd040c7bc0381d1dfd89dc72a
7
+ data.tar.gz: 3ac676cd7c0df46eb10a703967f9375d51674f6a61f3a8ae637ea8cd7562b6361905def86aa52269af8dfcdfc1cbd29a9e4ea2eee8665ff35a1341207247d7fa
data/.rspec CHANGED
@@ -1,2 +1,3 @@
1
1
  --format documentation
2
2
  --color
3
+ --order random
data/.travis.yml CHANGED
@@ -1,14 +1,15 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.9.3
4
3
  - 2.0.0
5
4
  - 2.1.0
5
+ - 2.2.0
6
6
  - ruby-head
7
7
  - jruby
8
8
  - jruby-head
9
9
  gemfile:
10
- - gemfiles/Gemfile.mongoid-3.0
11
10
  - gemfiles/Gemfile.mongoid-4.0
11
+ - gemfiles/Gemfile.mongoid-5.0
12
12
  - Gemfile
13
13
  services:
14
- - mongodb
14
+ - mongodb
15
+ sudo: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ ## 1.0.0
2
+
3
+ ### New Features
4
+
5
+ * Adds support for mongoid 5
6
+
7
+ ### Major Changes (Backwards Incompatible)
8
+
9
+ * Drops support for mongoid 3
10
+
11
+ * An optional tenant is now automatically set if a current tenant is defined.
12
+
13
+ * A unique constraint with an optional tenant now uses the client scoping. An item cannot be shared if another client item has the same value.
data/Gemfile CHANGED
@@ -1,15 +1,15 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem 'mongoid', '~> 4.0'
3
+ gem 'mongoid', '~> 5.0'
4
4
 
5
5
  gem 'rake', '~> 10.0'
6
6
 
7
7
  group :test do
8
- gem 'coveralls', :require => false
9
- gem 'rspec', '~> 2.12'
8
+ gem 'coveralls', require: false
9
+ gem 'rspec', '~> 3.1'
10
10
  gem 'yard', '~> 0.8'
11
- gem 'mongoid-rspec', '~> 1.5'
12
- gem 'database_cleaner', '~> 1.0'
11
+ gem 'mongoid-rspec', '~> 3.0'
12
+ gem 'rubocop', require: false
13
13
  end
14
14
 
15
15
  # Specify your gem's dependencies in mongoid-multitenancy.gemspec
data/README.md CHANGED
@@ -1,16 +1,16 @@
1
- # mongoid-multitenancy [![Build Status](https://travis-ci.org/PerfectMemory/mongoid-multitenancy.png?branch=master)](https://travis-ci.org/PerfectMemory/mongoid-multitenancy.png?branch=master) [![Coverage Status](https://coveralls.io/repos/PerfectMemory/mongoid-multitenancy/badge.png?branch=master)](https://coveralls.io/r/PerfectMemory/mongoid-multitenancy) [![Code Climate](https://codeclimate.com/github/PerfectMemory/mongoid-multitenancy.png)](https://codeclimate.com/github/PerfectMemory/mongoid-multitenancy) [![Dependency Status](https://gemnasium.com/PerfectMemory/mongoid-multitenancy.png)](https://gemnasium.com/PerfectMemory/mongoid-multitenancy)
1
+ # mongoid-multitenancy [![Build Status](https://travis-ci.org/PerfectMemory/mongoid-multitenancy.png?branch=master)](https://travis-ci.org/PerfectMemory/mongoid-multitenancy) [![Coverage Status](https://coveralls.io/repos/PerfectMemory/mongoid-multitenancy/badge.svg?branch=master&service=github)](https://coveralls.io/github/PerfectMemory/mongoid-multitenancy?branch=master) [![Code Climate](https://codeclimate.com/github/PerfectMemory/mongoid-multitenancy.png)](https://codeclimate.com/github/PerfectMemory/mongoid-multitenancy) [![Dependency Status](https://gemnasium.com/PerfectMemory/mongoid-multitenancy.png)](https://gemnasium.com/PerfectMemory/mongoid-multitenancy)
2
2
 
3
- mongoid-multitenancy adds the ability to scope [Mongoid](https://github.com/mongoid/mongoid) models to a tenant in a **shared database strategy**. Tenants are represented by a tenant model, such as `Client`. mongoid-multitenancy will help you set the current tenant on each request and ensures all 'tenant models' are always properly scoped to the current tenant: when viewing, searching and creating.
3
+ mongoid-multitenancy adds the ability to scope [Mongoid](https://github.com/mongoid/mongoid) models to a tenant in a **shared database strategy**. Tenants are represented by a tenant model, such as `Client`. mongoid-multitenancy will help you set the current tenant on each request and ensures that all 'tenant models' are always properly scoped to the current tenant: when viewing, searching and creating.
4
4
 
5
5
  It is directly inspired by the [acts_as_tenant gem](https://github.com/ErwinM/acts_as_tenant) for Active Record.
6
6
 
7
7
  In addition, mongoid-multitenancy:
8
8
 
9
9
  * allows you to set the current tenant
10
- * redefines some mongoid functions like `index`, `validates_with` and `delete_all` to take in account the multitenancy
11
10
  * allows shared items between the tenants
12
11
  * allows you to define an immutable tenant field once it is persisted
13
- * is thread safe.
12
+ * is thread safe
13
+ * redefines some mongoid functions like `index`, `validates_with` and `delete_all` to take in account the multitenancy.
14
14
 
15
15
  Installation
16
16
  ===============
@@ -30,7 +30,7 @@ Or install it yourself as:
30
30
  Usage
31
31
  ===============
32
32
 
33
- There are two steps in adding multi-tenancy to your app with acts_as_tenant:
33
+ There are two steps to add multi-tenancy to your app with mongoid-multitenancy:
34
34
 
35
35
  1. setting the current tenant and
36
36
  2. scoping your models.
@@ -87,7 +87,8 @@ The association passed to the `tenant` function must be valid.
87
87
 
88
88
  * :optional : set to true when the tenant is optional (default value is `false`)
89
89
  * :immutable : set to true when the tenant field is immutable (default value is `true`)
90
- * :class_name, etc. : all the other options will be passed to the mongoid relation
90
+ * :full_indexes : set to true to add the tenant field automatically to all the indexes (default value is `true`)
91
+ * :class_name, etc. : all the other options will be passed to the mongoid relation (belongs_to)
91
92
 
92
93
  Some examples to illustrate this behavior:
93
94
 
@@ -110,7 +111,9 @@ article.valid? # => false
110
111
 
111
112
  **Optional tenant**
112
113
 
113
- When setting an optional tenant, for example to allow shared instances between all the tenants, the default scope will return both the tenant and the free-tenant items. That means that using `Article.delete_all` or `Article.destroy_all` will **remove the shared items too**. And that means too that **the tenant must be set manually**.
114
+ When setting an optional tenant, for example to allow shared instances between all the tenants, the default scope will return both the tenant and the free-tenant items. That means that using `Article.delete_all` or `Article.destroy_all` will **remove the shared items too**.
115
+
116
+ Note: if a current tenant is set and you want to mark the item shared, you must explicitly set the tenant relation to nil after the initialization.
114
117
 
115
118
  ```ruby
116
119
  class Article
@@ -125,11 +128,13 @@ end
125
128
  Mongoid::Multitenancy.with_tenant(client_instance) do
126
129
  Article.all # => all articles where client_id.in [50ca04b86c82bfc125000025, nil]
127
130
  article = Article.new(:title => 'New article')
128
- article.save # => <#Article _id: 50ca04b86c82bfc125000044, title: 'New blog', client_id: nil>
131
+ article.save # => <#Article _id: 50ca04b86c82bfc125000044, title: 'New blog', client_id: 50ca04b86c82bfc125000025>
129
132
 
130
- # tenant needs to be set manually
131
- article.tenant = client_instance
132
- article.save => <#Article _id: 50ca04b86c82bfc125000044, title: 'New blog', client_id: 50ca04b86c82bfc125000025>
133
+ # tenant needs to be set manually to nil
134
+ article = Article.new(:title => 'New article', :client => nil)
135
+ article.save # => <#Article _id: 50ca04b86c82bfc125000044, title: 'New blog', client_id: 50ca04b86c82bfc125000025>
136
+ article.tenant = nil
137
+ article.save => <#Article _id: 50ca04b86c82bfc125000044, title: 'New blog', client_id: nil>
133
138
  end
134
139
  ```
135
140
 
@@ -156,19 +161,43 @@ Setting the current_tenant yourself requires you to use a before_filter to set t
156
161
  Mongoid Uniqueness validators
157
162
  -------------------
158
163
 
159
- mongoid-multitenancy will automatically add the tenant foreign key in the scope list for each of uniqueness validators in order
160
- to avoid to redefine all your validators.
164
+ mongoid-multitenancy brings a TenantUniqueness validator that will, depending on the tenant options, check that your uniqueness
165
+ constraints are respected:
166
+
167
+ * When used with a *mandatory* tenant, the uniqueness constraint is scoped to the current client.
168
+
169
+ In the following case, 2 articles can have the same slug if they belongs to 2 different clients.
161
170
 
162
171
  ```ruby
163
172
  class Article
164
173
  include Mongoid::Document
165
174
  include Mongoid::Multitenancy::Document
166
175
 
167
- tenant(:client)
176
+ tenant :client
177
+
178
+ field :slug
179
+
180
+ validates_tenant_uniqueness_of :slug
181
+ end
182
+ ```
183
+
184
+ * When used with an *optional* tenant, the uniqueness constraint is not scoped if the item is shared, but is
185
+ scoped to the client new item otherwise. Note that a private item cannot have the the value if a shared item
186
+ already uses it.
187
+
188
+ In the following case, 2 private articles can have the same slug if they belongs to 2 different clients. But if a shared
189
+ article has the slug "slugA", no client will be able to use that slug again, like a standard validates_uniqueness_of does.
190
+
191
+ ```ruby
192
+ class Article
193
+ include Mongoid::Document
194
+ include Mongoid::Multitenancy::Document
195
+
196
+ tenant :client, optional: true
168
197
 
169
198
  field :slug
170
199
 
171
- validates_uniqueness_of :slug # => :scope => client_id is added automatically
200
+ validates_tenant_uniqueness_of :slug
172
201
  end
173
202
  ```
174
203
 
data/Rakefile CHANGED
@@ -5,4 +5,4 @@ require 'yard'
5
5
  YARD::Rake::YardocTask.new
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
- task :default => :spec
8
+ task default: :spec
@@ -1,15 +1,14 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem 'mongoid', '~> 4.0.alpha'
3
+ gem 'mongoid', '~> 4.0'
4
4
 
5
5
  gem 'rake', '~> 10.0'
6
6
 
7
7
  group :test do
8
8
  gem 'coveralls', :require => false
9
- gem 'rspec', '~> 2.12'
9
+ gem 'rspec', '~> 3.1'
10
10
  gem 'yard', '~> 0.8'
11
- gem 'mongoid-rspec', '~> 1.5'
12
- gem 'database_cleaner', '~> 1.0'
11
+ gem 'mongoid-rspec', '~> 2.1'
13
12
  end
14
13
 
15
14
  # Specify your gem's dependencies in mongoid-multitenancy.gemspec
@@ -1,15 +1,14 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem 'mongoid', '~> 3.0'
3
+ gem 'mongoid', '~> 5.0'
4
4
 
5
5
  gem 'rake', '~> 10.0'
6
6
 
7
7
  group :test do
8
8
  gem 'coveralls', :require => false
9
- gem 'rspec', '~> 2.12'
9
+ gem 'rspec', '~> 3.1'
10
10
  gem 'yard', '~> 0.8'
11
- gem 'mongoid-rspec', '~> 1.5'
12
- gem 'database_cleaner', '~> 1.0'
11
+ gem 'mongoid-rspec', '~> 3.0'
13
12
  end
14
13
 
15
14
  # Specify your gem's dependencies in mongoid-multitenancy.gemspec
@@ -1,17 +1,13 @@
1
- require "mongoid"
2
- require "mongoid/multitenancy/document"
3
- require "mongoid/multitenancy/version"
4
- require "mongoid/validators/tenant_validator"
1
+ require 'mongoid'
2
+ require 'mongoid/multitenancy/document'
3
+ require 'mongoid/multitenancy/version'
4
+ require 'mongoid/multitenancy/validators/tenancy'
5
+ require 'mongoid/multitenancy/validators/tenant_uniqueness'
5
6
 
6
7
  module Mongoid
7
8
  module Multitenancy
8
9
  class << self
9
10
 
10
- # Returns true if using Mongoid 4
11
- def mongoid4?
12
- Mongoid::VERSION.start_with? '4'
13
- end
14
-
15
11
  # Set the current tenant. Make it Thread aware
16
12
  def current_tenant=(tenant)
17
13
  Thread.current[:current_tenant] = tenant
@@ -25,7 +21,7 @@ module Mongoid
25
21
  # Affects a tenant temporary for a block execution
26
22
  def with_tenant(tenant, &block)
27
23
  if block.nil?
28
- raise ArgumentError, "block required"
24
+ raise ArgumentError, 'block required'
29
25
  end
30
26
 
31
27
  old_tenant = self.current_tenant
@@ -6,64 +6,88 @@ module Mongoid
6
6
  module ClassMethods
7
7
  attr_accessor :tenant_field, :tenant_options
8
8
 
9
+ # List of authorized options
9
10
  MULTITENANCY_OPTIONS = [:optional, :immutable, :full_indexes, :index]
10
11
 
12
+ # Defines the tenant field for the document.
13
+ #
14
+ # @example Define a tenant.
15
+ # tenant :client, optional: false, immutable: true, full_indexes: true
16
+ #
17
+ # @param [ Symbol ] name The name of the relation.
18
+ # @param [ Hash ] options The relation options.
19
+ # All the belongs_to options are allowed plus the following ones:
20
+ #
21
+ # @option options [ Boolean ] :full_indexes If true the tenant field
22
+ # will be added for each index.
23
+ # @option options [ Boolean ] :immutable If true changing the tenant
24
+ # wil raise an Exception.
25
+ # @option options [ Boolean ] :optional If true allow the document
26
+ # to be shared among all the tenants.
27
+ #
28
+ # @return [ Field ] The generated field
11
29
  def tenant(association = :account, options = {})
12
- options = { full_indexes: true, immutable: true }.merge(options)
30
+ options = { full_indexes: true, immutable: true }.merge!(options)
31
+ assoc_options, multitenant_options = build_options(options)
13
32
 
14
33
  # Setup the association between the class and the tenant class
15
- belongs_to association, extract_association_options(options)
34
+ belongs_to association, assoc_options
16
35
 
17
36
  # Get the tenant model and its foreign key
18
37
  self.tenant_field = reflect_on_association(association).foreign_key
19
- self.tenant_options = extract_tenant_options(options)
38
+ self.tenant_options = multitenant_options
20
39
 
21
40
  # Validates the tenant field
22
- validates tenant_field, tenant: options
41
+ validates_tenancy_of tenant_field, multitenant_options
23
42
 
24
- # Set the default_scope to scope to current tenant
25
- default_scope lambda {
26
- if Multitenancy.current_tenant
27
- if options[:optional]
28
- where(self.tenant_field.to_sym.in => [Multitenancy.current_tenant.id, nil])
29
- else
30
- where(self.tenant_field => Multitenancy.current_tenant.id)
31
- end
32
- else
33
- where(nil)
34
- end
35
- }
36
-
37
- self.define_singleton_method(:inherited) do |child|
38
- child.tenant association, options
39
- super(child)
40
- end
41
-
42
- if options[:index]
43
- index({self.tenant_field => 1}, { background: true })
44
- end
43
+ define_default_scope
44
+ define_initializer association
45
+ define_inherited association, options
46
+ define_index if multitenant_options[:index]
45
47
  end
46
48
 
47
- # Redefine 'validates_with' to add the tenant scope when using a UniquenessValidator
48
- def validates_with(*args, &block)
49
- if !self.tenant_options[:optional]
50
- validator = if Mongoid::Multitenancy.mongoid4?
51
- Validatable::UniquenessValidator
52
- else
53
- Validations::UniquenessValidator
54
- end
55
-
56
- if args.first.ancestors.include?(validator)
57
- args.last[:scope] = Array(args.last[:scope]) << self.tenant_field
58
- end
59
- end
49
+ # Validates whether or not a field is unique against the documents in the
50
+ # database.
51
+ #
52
+ # @example
53
+ #
54
+ # class Person
55
+ # include Mongoid::Document
56
+ # include Mongoid::Multitenancy::Document
57
+ # field :title
58
+ #
59
+ # validates_tenant_uniqueness_of :title
60
+ # end
61
+ #
62
+ # @param [ Array ] *args The arguments to pass to the validator.
63
+ def validates_tenant_uniqueness_of(*args)
64
+ validates_with(TenantUniquenessValidator, _merge_attributes(args))
65
+ end
60
66
 
61
- super(*args, &block)
67
+ # Validates whether or not a tenant field is correct.
68
+ #
69
+ # @example Define the tenant validator
70
+ #
71
+ # class Person
72
+ # include Mongoid::Document
73
+ # include Mongoid::Multitenancy::Document
74
+ # field :title
75
+ # tenant :client
76
+ #
77
+ # validates_tenant_of :client
78
+ # end
79
+ #
80
+ # @param [ Array ] *args The arguments to pass to the validator.
81
+ def validates_tenancy_of(*args)
82
+ validates_with(TenancyValidator, _merge_attributes(args))
62
83
  end
63
84
 
64
85
  # Redefine 'index' to include the tenant field in first position
65
86
  def index(spec, options = nil)
66
- spec = { self.tenant_field => 1 }.merge(spec) if self.tenant_options[:full_indexes]
87
+ if tenant_options[:full_indexes]
88
+ spec = { tenant_field => 1 }.merge(spec)
89
+ end
90
+
67
91
  super(spec, options)
68
92
  end
69
93
 
@@ -74,24 +98,68 @@ module Mongoid
74
98
 
75
99
  private
76
100
 
77
- def extract_association_options(options)
78
- new_options = {}
101
+ # @private
102
+ def build_options(options)
103
+ assoc_options = {}
104
+ multitenant_options = {}
79
105
 
80
106
  options.each do |k, v|
81
- new_options[k] = v unless MULTITENANCY_OPTIONS.include?(k)
107
+ if MULTITENANCY_OPTIONS.include?(k)
108
+ multitenant_options[k] = v
109
+ else
110
+ assoc_options[k] = v
111
+ end
82
112
  end
83
113
 
84
- new_options
114
+ [assoc_options, multitenant_options]
85
115
  end
86
116
 
87
- def extract_tenant_options(options)
88
- new_options = {}
117
+ # @private
118
+ #
119
+ # Define the after_initialize
120
+ def define_initializer(association)
121
+ # Apply the default value when the default scope is complex (optional tenant)
122
+ after_initialize lambda {
123
+ if Multitenancy.current_tenant && send(association.to_sym).nil?
124
+ send "#{association}=".to_sym, Multitenancy.current_tenant
125
+ end
126
+ }
127
+ end
89
128
 
90
- options.each do |k, v|
91
- new_options[k] = v if MULTITENANCY_OPTIONS.include?(k)
129
+ # @private
130
+ #
131
+ # Define the inherited method
132
+ def define_inherited(association, options)
133
+ define_singleton_method(:inherited) do |child|
134
+ child.tenant association, options
135
+ super(child)
92
136
  end
137
+ end
138
+
139
+ # @private
140
+ #
141
+ # Set the default scope
142
+ def define_default_scope
143
+ # Set the default_scope to scope to current tenant
144
+ default_scope lambda {
145
+ if Multitenancy.current_tenant
146
+ tenant_id = Multitenancy.current_tenant.id
147
+ if tenant_options[:optional]
148
+ where(tenant_field.to_sym.in => [tenant_id, nil])
149
+ else
150
+ where(tenant_field => tenant_id)
151
+ end
152
+ else
153
+ where(nil)
154
+ end
155
+ }
156
+ end
93
157
 
94
- new_options
158
+ # @private
159
+ #
160
+ # Create the index
161
+ def define_index
162
+ index({ tenant_field => 1 }, background: true)
95
163
  end
96
164
  end
97
165
  end