required_scopes 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4d5c01aa1fe08e48a58a5d0b04403fdb1776cde9
4
+ data.tar.gz: a5e438c3698e6def089b4d451bb4dc2651c00ad3
5
+ SHA512:
6
+ metadata.gz: 5dc8c8bc939f6932c84408f7dfcc4a5bcc811373a110919818dc3a0f94723b629cdf35913b7137997277dd6b959ed0affdc5345fecbaae85b3198d1bf6a0bf76
7
+ data.tar.gz: cccbab92e292d84397b6f8e29c102d0e8e136cccac43ca7c83e893f14644faed1e0b72d577b15e9388825c9bdd2734fa2dde63d9e4592cf61761f0a07661f2c5
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ /spec_database_config.rb*
data/.travis.yml ADDED
@@ -0,0 +1,29 @@
1
+ before_install:
2
+ - gem install rubygems-update -v2.1.11
3
+ - gem update --system 2.1.11
4
+ - gem --version
5
+ rvm:
6
+ - "1.8.7"
7
+ - "1.9.3"
8
+ - "2.0.0"
9
+ - "jruby-1.7.6"
10
+ env:
11
+ - REQUIRED_SCOPES_AR_TEST_VERSION=3.2.16 REQUIRED_SCOPES_TRAVIS_CI_DATABASE_TYPE=mysql
12
+ - REQUIRED_SCOPES_AR_TEST_VERSION=3.2.16 REQUIRED_SCOPES_TRAVIS_CI_DATABASE_TYPE=postgres
13
+ - REQUIRED_SCOPES_AR_TEST_VERSION=3.2.16 REQUIRED_SCOPES_TRAVIS_CI_DATABASE_TYPE=sqlite
14
+ - REQUIRED_SCOPES_AR_TEST_VERSION=4.0.2 REQUIRED_SCOPES_TRAVIS_CI_DATABASE_TYPE=mysql
15
+ - REQUIRED_SCOPES_AR_TEST_VERSION=4.0.2 REQUIRED_SCOPES_TRAVIS_CI_DATABASE_TYPE=postgres
16
+ - REQUIRED_SCOPES_AR_TEST_VERSION=4.0.2 REQUIRED_SCOPES_TRAVIS_CI_DATABASE_TYPE=sqlite
17
+ before_script:
18
+ # - export JRUBY_OPTS="-J-Xmx256m -J-Xms256m $JRUBY_OPTS"
19
+ - mysql -e 'create database myapp_test;'
20
+ - psql -c 'create database myapp_test;' -U postgres
21
+ matrix:
22
+ exclude:
23
+ # ActiveRecord 4.x doesn't support Ruby 1.8.7
24
+ - rvm: 1.8.7
25
+ env: REQUIRED_SCOPES_AR_TEST_VERSION=4.0.2 REQUIRED_SCOPES_TRAVIS_CI_DATABASE_TYPE=mysql
26
+ - rvm: 1.8.7
27
+ env: REQUIRED_SCOPES_AR_TEST_VERSION=4.0.2 REQUIRED_SCOPES_TRAVIS_CI_DATABASE_TYPE=postgres
28
+ - rvm: 1.8.7
29
+ env: REQUIRED_SCOPES_AR_TEST_VERSION=4.0.2 REQUIRED_SCOPES_TRAVIS_CI_DATABASE_TYPE=sqlite
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in required_scopes.gemspec
4
+ gemspec
5
+
6
+ ar_version = ENV['REQUIRED_SCOPES_AR_TEST_VERSION']
7
+ ar_version = ar_version.strip if ar_version
8
+
9
+ version_spec = case ar_version
10
+ when nil then nil
11
+ when 'master' then { :git => 'git://github.com/rails/activerecord.git' }
12
+ else "=#{ar_version}"
13
+ end
14
+
15
+ if version_spec
16
+ gem("activerecord", version_spec)
17
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013-2014 Andrew Geweke
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,277 @@
1
+ # RequiredScopes
2
+
3
+ RequiredScopes keeps developers from being able to accidentally forget about critical scopes on database tables. For
4
+ example:
5
+
6
+ * If you provide software-as-a-service to many clients, forgetting to include the client ID in your query may leak
7
+ data from one client to another — potentially a truly disastrous thing to happen.
8
+ * If you store time-series data in a table that gets very large, querying without including a time range can cause
9
+ huge scalability problems as you accidentally scan the entire table.
10
+ * If you associate permissions with various records in a table, then forgetting to constrain on the permissions in a
11
+ query results in them being completely ineffective.
12
+ * If you soft-delete records (via a `deleted_at` or `deleted` flag), forgetting to either explicitly include or exclude
13
+ deleted records can result in "deleted" data reappearing, which can be very bad.
14
+
15
+ ...and the list goes on.
16
+
17
+ RequiredScopes works by letting you create one or more _required scope categories_, each named via a symbol
18
+ (_e.g._, `:client_id`, `:time_range`, `:permissions`, or `:deleted`). You can then declare scopes (or class methods)
19
+ as _satisfying_ one or more categories. When time comes to query the database, at least one scope satisfying each
20
+ category must have been used, or else an exception will be raised.
21
+
22
+ For example:
23
+
24
+ class StatusUpdate < ActiveRecord::Base
25
+ must_scope_by :client_id, :recency
26
+
27
+ scope :last_week, lambda { where("created_at >= ?", 1.week.ago) }, :satisfies => :recency
28
+ scope :last_month, lambda { where("created_at >= ?", 1.month.ago) }, :satisfies => :recency
29
+
30
+ class << self
31
+ def for_client(client_id)
32
+ where(:client_id => client_id).scope_category_satisfied(:client_id)
33
+ end
34
+ end
35
+ end
36
+
37
+ StatusUpdate.last(100) # => RequiredScopes::Errors::RequiredScopeCategoriesNotSatisfiedError
38
+ StatusUpdate.for_client(982).last_week # => [ <StatusUpdate id:321890414>, <StatusUpdate id:321890583>, ... ]
39
+
40
+ There's much more, too. For example, it's trivial to skip these checks if you want &mdash; the idea is to keep
41
+ developers from simply _forgetting_ about scoping, not to make their lives more difficult. See below, under **Usage**,
42
+ for more information.
43
+
44
+ #### As an alternative to `default_scope`
45
+
46
+ RequiredScopes was actually born as an alternative to `default_scope`. Rails' `default_scope` is a great idea, but, in
47
+ actual usage, we've seen it cause a surprisingly large number of bugs. (It works exactly the way it claims it does,
48
+ but it turns out this isn't actually a great way for it to work.)
49
+
50
+ What goes wrong? It turns out that there's almost never a single scope that you truly want applied 100%, or even 99%,
51
+ of the time. Instead, it's more like 85%; but, because it's the default, developers almost always completely forget
52
+ about it, and bugs result.
53
+
54
+ For example, take the classic case of a `deleted_at` column on a User model, and
55
+ `default_scope lambda { where('deleted_at IS NULL')} }`. This works great for most "normal" functions of the
56
+ application. But, then, the edge cases start creeping in:
57
+
58
+ * In your admin controllers, where you _want_ to be able to see deleted users, you keep forgetting to apply `#unscoped`
59
+ &mdash; and lots of errors on `nil` result.
60
+ * When a new user signs up and chooses a username, you forget to unscope when checking if an existing user has that
61
+ username, resulting in database unique-index failures on `users.username` when creating a new user (and HTTP 500
62
+ pages returned to the end user).
63
+ * Your "reset password" page almost certainly wants to find a user account even if it's deleted, and (at minimum)
64
+ display a message telling the user they have a deleted account. You don't want to act like the user simply doesn't
65
+ exist.
66
+
67
+ The truth is, there _isn't_ a single default scope that can ever be safely applied across-the-board. Developers have to
68
+ think about it, every single time. This isn't hard; it takes an extra second or two, and prevents hours of debugging
69
+ time (and lots of user frustration at bugs that would've resulted). But base Rails only lets you decide to either apply
70
+ a single `default_scope` across the board (where you run into the above problems) or not (where you run into even worse
71
+ ones, as developers completely forget about your `deleted_at` column, or whatever).
72
+
73
+ Hence, RequiredScopes. It prevents you from forgetting about critical scopes, yet doesn't try to shoehorn a single
74
+ `default_scope` everywhere.
75
+
76
+ #### Supported Versions
77
+
78
+ RequiredScopes supports:
79
+
80
+ * Ruby 1.8.7, 1.9.3, 2.0.0, 2.1.0, and JRuby 1.7.9.
81
+ * ActiveRecord 3.2.x and 4.0.x.
82
+ * Any database that works with ActiveRecord.
83
+
84
+ Note that because RequiredScopes ties in quite tightly with ActiveRecord, supporting previous ActiveRecord versions
85
+ would be significant work. Patches are always welcome. :-)
86
+
87
+ Current build status: ![Current Build Status](https://api.travis-ci.org/ageweke/required_scopes.png?branch=master)
88
+
89
+ ## Installation
90
+
91
+ Add this line to your application's Gemfile:
92
+
93
+ gem 'required_scopes'
94
+
95
+ And then execute:
96
+
97
+ $ bundle
98
+
99
+ Or install it yourself as:
100
+
101
+ $ gem install required_scopes
102
+
103
+ ## Usage
104
+
105
+ #### `base_scope_required!`, or, the simple version
106
+
107
+ An example:
108
+
109
+ class User < ActiveRecord::Base
110
+ base_scope_required!
111
+
112
+ # #base_scope is just like #scope, except that it says "this scope satisfies base_scope_required!"
113
+ base_scope :deleted, lambda { where("deleted_at IS NOT NULL") }
114
+ base_scope :not_deleted, lambda { where("deleted_at IS NULL") }
115
+
116
+ # #scope does not satisfy the requirement
117
+ scope :young, lambda { where("age <= 20") }
118
+
119
+ class << self
120
+ def deleted_recently
121
+ # The call to #base_scope_satisfied says "the scope I'm returning satisfies #base_scope_required!"
122
+ where("deleted_at >= ?", 1.week.ago).base_scope_satisfied
123
+ end
124
+ end
125
+ end
126
+
127
+ This sets up the following behavior:
128
+
129
+ # Forgetting the scope gives you an error
130
+ User.first # => RequiredScopes::Errors::BaseScopeNotSatisfiedError
131
+ User.young.first # => RequiredScopes::Errors::BaseScopeNotSatisfiedError
132
+
133
+ # Any of the declared scopes work just fine
134
+ User.deleted.first # => SELECT * FROM users WHERE deleted_at IS NOT NULL LIMIT 1
135
+ User.not_deleted.first # => SELECT * FROM users WHERE deleted_at IS NULL LIMIT 1
136
+ User.deleted_recently.first # => SELECT * FROM users WHERE deleted_at >= '2013-12-30 05:23:01.367705' LIMIT 1
137
+
138
+ # You can chain them like normal, and use them anywhere
139
+ User.where(:name => 'some user').not_deleted.first
140
+ # => SELECT * FROM users WHERE deleted_at IS NULL AND name = 'some user' LIMIT 1
141
+
142
+ # Pass them around, build on them...they work just like any other scope
143
+ s = User.where(:name => 'some user')
144
+ s.not_deleted.first
145
+ # => SELECT * FROM users WHERE deleted_at IS NULL AND name = 'some user' LIMIT 1
146
+ s.deleted.first
147
+ # => SELECT * FROM users WHERE deleted_at IS NOT NULL AND name = 'some user' LIMIT 1
148
+ s.deleted_recently.where("age > 20").first
149
+ # => SELECT * FROM users WHERE deleted_at >= '2013-12-30 05:23:01.367705' AND name = 'some user' AND age > 20 LIMIT 1
150
+
151
+ # The special scope "ignoring_base" is generated for you; it satisfies the requirement without constraining in any way
152
+ User.ignoring_base.first # => SELECT * FROM users LIMIT 1
153
+ # #base_scope_satisfied automatically satisfies the requirement, without constraining in any way
154
+ User.base_scope_satisfied.first # => SELECT * FROM users LIMIT 1
155
+
156
+ #### `must_scope_by`, or, the general case
157
+
158
+ `base_scope_required!` and `base_scope` are actually just syntactic sugar on top of a more general system that lets
159
+ you declare one or more _scope categories_ (each of which is just a symbol) and various scopes and class methods that
160
+ _satisfy_ those categories.
161
+
162
+ (`base_scope_required!` is exactly equivalent to `must_scope_by :base`, and `base_scope :foo, lambda { ... }` is
163
+ exactly equivalent to `scope :foo, lambda { ... }, :satisfies => :base`.)
164
+
165
+ For example:
166
+
167
+ class User < ActiveRecord::Base
168
+ must_scope_by :deleted, :client
169
+
170
+ scope :not_deleted, lambda { where("deleted_at IS NULL") }, :satisfies => :deleted
171
+ scope :deleted, lambda { where("deleted_at IS NOT NULL") }, :satisfies => :deleted
172
+
173
+ scope :admin_active, lambda { where("deleted_at IS NULL AND client_id = 0") }, :satisfies => [ :deleted, :client ]
174
+
175
+ class << self
176
+ def for_client(c)
177
+ where(:client_id => c.id).scope_category_satisfied(:client)
178
+ end
179
+
180
+ def active_for_client(c)
181
+ where(:client_id => c.id, :deleted_at => nil).scope_categories_satisfied(:client, :deleted)
182
+ end
183
+ end
184
+ end
185
+
186
+ This sets up two categories of scopes that _both_ must be satisfied before you can query the database, `:deleted` and
187
+ `:client`. The scopes `not_deleted` and `deleted` satisfy the `:deleted` category; the scope `admin_active` satisfies
188
+ both. The class method `for_client` satisfies the `:client` category; the class method `active_for_client` satisfies
189
+ both categories.
190
+
191
+ For each required category, a special `ignoring` scope is automatically defined &mdash; `ignoring_deleted`
192
+ and `ignoring_client` in the above example. This tells RequiredScopes that you're explicitly deciding _not_ to apply
193
+ any scopes for the given category. So, while `User.not_deleted.first` will raise an exception complaining that you
194
+ haven't satisfied the `:client` category, `User.not_deleted.ignoring_client.first` will run just fine, and will not
195
+ constrain on client in any way.
196
+
197
+ All scopes get methods called `#scope_category_satisfied` and `#scope_categories_satisfied`. (You can actually pass
198
+ either a single scope or multiple scopes to either one.) These mark categories as satisfied, without constraining in
199
+ any way; this is useful for class methods, as above, that should be considered to satisfy a requirement. They also
200
+ function in block form, just like `#scoping` or `#unscoped` from ActiveRecord do:
201
+
202
+ User.scope_category_satisfied(:client) do
203
+ User.not_deleted.first # => SELECT * FROM users WHERE deleted_at IS NULL LIMIT 1
204
+ end
205
+
206
+ Note that the built-in ActiveRecord `#unscoped` method does not interact with RequiredScopes in any way. Unscoping
207
+ neither satisfies nor removes the satisfaction of any required categories.
208
+
209
+ #### RequiredScopes and Inheritance
210
+
211
+ If you use inheritance among your model classes, child classes will require any scope categories that their parents
212
+ have declared to require; if you add a separate `must_scope_by` call in the child class, then it will additionally
213
+ require those categories, too.
214
+
215
+ If you do _not_ want a child class to require all the categories of its parent, call
216
+ `ignore_parent_scope_requirement :client, :deleted` (or whatever categories you want to skip the requirement for) in
217
+ the child class. This removes the requirement from the child class.
218
+
219
+ #### How Smart Is It?
220
+
221
+ It's important to note that RequiredScopes does not, in any way, _actually look at your `WHERE` clauses_. That is, the
222
+ only thing it's doing is matching the categories you've said are required with scopes that satisfy those categories;
223
+ it does not know or care what those scopes actually _do_.
224
+
225
+ If you say a scope satisfies a category, then RequiredScopes will be happy with it, even if it actually just does
226
+ `ORDER BY id ASC` (or does nothing at all!). If no scope is applied that satisfies a category, you'll get an error,
227
+ even if you've constrained every column in seven different ways.
228
+
229
+ This hopefully makes the entire system much easier to understand, but it's worth noting.
230
+
231
+ #### Along With `default_scope`
232
+
233
+ Note that RequiredScopes does not affect the behavior of `default_scope` in any way; if you declare a `default_scope`,
234
+ it will still be used, as normal. `default_scope`s cannot satisfy any categories, however. (But this wouldn't make any
235
+ sense, anyway: if your default scope satisfies a category, then it's really not required any more, is it?)
236
+
237
+ ## Contributing
238
+
239
+ 1. Fork it
240
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
241
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
242
+ 4. Push to the branch (`git push origin my-new-feature`)
243
+ 5. Create new Pull Request
244
+
245
+ ### Running Specs
246
+
247
+ RequiredScopes is quite thoroughly tested, using RSpec. Note that all of its tests are "system" tests, in that they
248
+ test the entire Gem, all the way down through ActiveRecord, to the database. This is because there is really no
249
+ significant code in RequiredScopes that's independent of its interface to ActiveRecord, and so unit tests would be of
250
+ little use. (In other words, making this gem work correctly is all about getting the patches to ActiveRecord right,
251
+ not about consistency of any sophisticated internal logic.)
252
+
253
+ To run these specs, you'll need a database server up and running that ActiveRecord can talk to. The specs create and
254
+ drop various tables (all prefixed with `rec_spec_`, so they're highly unlikely to conflict with anything). Because
255
+ they do this, there's no need to prepare the database ahead of time, and it should be safe to use a database that's
256
+ also used for other things. (On the other hand, having its own dedicated database won't hurt; and if you run these
257
+ specs against a database containing data that's precious, you're just asking for it.)
258
+
259
+ To run these specs:
260
+
261
+ 1. If you want to test against a particular ActiveRecord version, `export REQUIRED_SCOPES_AR_TEST_VERSION=3.2.16` (for example). If you want the latest stable ActiveRecord, simply skip this step.
262
+ 2. `cd required_scopes` (the root of the gem).
263
+ 3. Create a file called `spec_database_config.rb` at the root level of the gem. It should define your connection to the database, like so:
264
+
265
+ REQUIRED_SCOPES_SPEC_DATABASE_CONFIG = {
266
+ :require => 'pg',
267
+ :database_gem_name => 'pg',
268
+ :config => {
269
+ :adapter => 'postgresql',
270
+ :database => 'some_database',
271
+ :username => 'postgres',
272
+ :password => 'some_password'
273
+ }
274
+ }
275
+
276
+ 4. `bundle install`. (This step must come _after_ you create `spec_database_config.rb`; it uses that file to know what database gem to include.)
277
+ 5. `bundle exec rspec spec` will run all specs. (Or just `rake`.)
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,17 @@
1
+ # There's currently nothing at all in the RequiredScopes module except for its use as a namespace.
2
+ module RequiredScopes
3
+ end
4
+
5
+ require "required_scopes/version"
6
+ require "required_scopes/active_record/base"
7
+ require "active_record"
8
+
9
+ # Add methods to ::ActiveRecord::Base that let you declare scoping requirements, and declare scopes that satisfy
10
+ # them...
11
+ ActiveRecord::Base.send(:include, ::RequiredScopes::ActiveRecord::Base)
12
+
13
+ # ...and add methods to ::ActiveRecord::Relation that enforce those requirements.
14
+ require "required_scopes/active_record/relation"
15
+
16
+ require "required_scopes/active_record/version_compatibility"
17
+ ::RequiredScopes::ActiveRecord::VersionCompatibility.apply_version_specific_fixes!
@@ -0,0 +1,235 @@
1
+ require 'active_support'
2
+ require 'active_record'
3
+
4
+ module RequiredScopes
5
+ module ActiveRecord
6
+ # This is the module that gets +include+d into ::ActiveRecord::Base when +required_scopes+ is loaded. It defines
7
+ # the exposed methods on ::ActiveRecord::Base, and overrides a few (like #scope and #unscoped).
8
+ module Base
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ class << self
13
+ delegate :scope_categories_satisfied, :scope_category_satisfied,
14
+ :all_scope_categories_satisfied, :to => :relation
15
+ end
16
+ end
17
+
18
+ # Calling #destroy ends up generating a relation, via this method, that is used to destroy the object. We need
19
+ # to make sure we don't trigger any checks on this call.
20
+ def relation_for_destroy
21
+ out = super
22
+ out.all_scope_categories_satisfied!
23
+ out
24
+ end
25
+
26
+ module ClassMethods
27
+ # Declares that all users of your model must scope it by one or more categories when running a query, performing
28
+ # a calculation (like #count or #exists?), or running certain bulk-update statements (like #delete_all).
29
+ # Categories are simply symbols, and they are considered satisfied if a scope is used that declares (_e.g._)
30
+ # <tt>:satisfies => :deletion</tt> is included before running a query.
31
+ #
32
+ # This is the heart of +required_scopes+. Its purpose is to remind developers that certain kinds of constraints
33
+ # should always be taken into account when accessing data in a particular table, so that they can't
34
+ # under-constrain queries and potentially ignore soft deletion of rows, or cross client boundaries, or similar
35
+ # things.
36
+ #
37
+ # For example:
38
+ #
39
+ # class User < ActiveRecord::Base
40
+ # must_scope_by :deletion
41
+ #
42
+ # scope :normal, lambda { where(:deleted => false) }, :satisfies => :deletion
43
+ # scope :deleted, lambda { where(:deleted => true) }, :satisfies => :deletion
44
+ # end
45
+ #
46
+ # Now, if you say
47
+ #
48
+ # the_user = User.where(:name => 'foo').first
49
+ #
50
+ # ...you'll get a RequiredScopes::Errors::RequiredScopeCategoriesNotSatisfiedError; you must instead call
51
+ # one of these:
52
+ #
53
+ # the_user = User.normal.where(:name => 'foo').first
54
+ # the_user = User.deleted.where(:name => 'foo').first
55
+ #
56
+ # For any given scope category, a scope starting with +ignoring_+ is automatically created; this does not
57
+ # actually constrain the scope in any way, but marks that category as satisfied. (The point is not to _force_
58
+ # developers to constrain on something, but to make sure they can't simply forget about the category.) So this
59
+ # will also work:
60
+ #
61
+ # the_user = User.ignoring_deletion.where(:name => 'foo').first
62
+ #
63
+ # An explicit call to #unscoped removes all requirements, whether used in its direct form or its
64
+ # block form, so these will also both work:
65
+ #
66
+ # the_user = User.unscoped.where(:name => 'foo').first
67
+ # User.unscoped { the_user = User.where(:name => 'foo').first }
68
+ #
69
+ # ActiveRecord also lets you use class methods as scopes; if you want one of these to count as satisfying a
70
+ # scope category, use #scope_category_satisfied (or #scope_categories_satisfied):
71
+ #
72
+ # class User < ActiveRecord::Base
73
+ # must_scope_by :client
74
+ #
75
+ # scope :active_clients, lambda { where(:client_active => true) }, :satisfies => :client
76
+ #
77
+ # class << self
78
+ # def for_client_named(client_name)
79
+ # client_id = CLIENT_MAP[client_name]
80
+ # where(:client_id => client_id).scope_category_satisfied(:client)
81
+ # end
82
+ # end
83
+ # end
84
+ #
85
+ # In the above example, either <tt>User.active_clients.first</tt> or <tt>User.for_client_named('foo').first</tt>
86
+ # will count as having satisfied the requirement to scope by +:client+, and hence will not raise an error.
87
+ def must_scope_by(*args)
88
+ categories = args.map(&:to_sym)
89
+ @required_scope_categories ||= [ ]
90
+ @required_scope_categories += categories
91
+
92
+ categories.each do |category|
93
+ scope "ignoring_#{category}", lambda { send(::RequiredScopes::ActiveRecord::VersionCompatibility.relation_method_for_ignoring_scopes) }, :satisfies => category
94
+ end
95
+ end
96
+
97
+ # Returns the set of scope categories that must be satisfied for this class, as a (possibly-empty) Array.
98
+ def required_scope_categories
99
+ if self == ::ActiveRecord::Base
100
+ [ ]
101
+ else
102
+ out = (@required_scope_categories || [ ]) | superclass.required_scope_categories
103
+ out - (@ignored_parent_scope_requirements || [ ])
104
+ end
105
+ end
106
+
107
+ # If you're using inheritance in your ActiveRecord::Base classes, and a parent class declares a required scope
108
+ # category (using #must_scope_by), you can remove that requirement in a subclass using
109
+ # #ignore_parent_scope_requirement. (This should be quite rare.)
110
+ def ignore_parent_scope_requirement(*args)
111
+ categories = args.map(&:to_sym)
112
+ @ignored_parent_scope_requirements ||= [ ]
113
+ @ignored_parent_scope_requirements |= categories
114
+ end
115
+
116
+ # We want #unscoped to remove any actual scoping rules, just like it does in ActiveRecord by default. However,
117
+ # we *don't* want it to remove any category satisfaction that we're currently under. Why? So you can do this:
118
+ #
119
+ # User.all_scope_categories_satisfied do
120
+ # ...
121
+ # User.unscoped.find(...)
122
+ # ...
123
+ # end
124
+ def unscoped
125
+ satisfied_by_default = current_scope.try(:satisfied_scope_categories) || [ ]
126
+
127
+ out = super
128
+ out = out.scope_category_satisfied(satisfied_by_default) if satisfied_by_default.length > 0
129
+ out
130
+ end
131
+
132
+
133
+ # Declares that use of this ActiveRecord::Base class must be scoped by at least one _base scope_; a base scope
134
+ # is any scope declared using #base_scope, instead of #scope. (Other than satisfying this requirement,
135
+ # #base_scope behaves identically to #scope.) This can be used to ensure that developers don't forget to scope
136
+ # out deleted records, or don't forget to scope by client, or so forth. (See the +README+ for +required_scopes+
137
+ # for why this is a better solution in many cases than just using +default_scope+.)
138
+ #
139
+ # For example:
140
+ #
141
+ # class User < ActiveRecord::Base
142
+ # base_scope_required!
143
+ #
144
+ # base_scope :undeleted { where(:deleted => false) }
145
+ # base_scope :deleted { where(:deleted => true) }
146
+ # end
147
+ #
148
+ # Now, you'll get the following:
149
+ #
150
+ # User.where(...).first # RequiredScopes::Errors::BaseScopeNotSatisfiedError
151
+ #
152
+ # User.undeleted.where(...).first # => SELECT * FROM users WHERE deleted = 0 AND ... LIMIT 1
153
+ # User.deleted.where(...).first # => SELECT * FROM users WHERE deleted = 1 AND ... LIMIT 1
154
+ # User.ignoring_base.where(...).first # => SELECT * FROM users WHERE ... LIMIT 1
155
+ #
156
+ # (This is simply syntactic sugar on top of #must_scope_by, using a category name of +:base+; see that method
157
+ # for more details.)
158
+ def base_scope_required!
159
+ must_scope_by :base
160
+ end
161
+
162
+ # Declares a scope that satisfies the requirement introduced by #base_scope_required!. In all other ways, it
163
+ # behaves identically to ActiveRecord::Base#scope.
164
+ def base_scope(name, body, &block)
165
+ scope(name, body, :satisfies => :base, &block)
166
+ end
167
+
168
+ # Returns a scope identical to the one it's called on, but that's marked as satisfying the requirement
169
+ # introduced by #base_scope_required!. This is useful in class methods that return a scope you want to count
170
+ # as satisfying that requirement:
171
+ #
172
+ # class User < ActiveRecord::Base
173
+ # base_scope_required!
174
+ #
175
+ # class << self
176
+ # def for_client_named(c)
177
+ # where(:client_id => CLIENT_ID_TO_NAME_MAP[c]).base_scope_satisfied
178
+ # end
179
+ # end
180
+ # end
181
+ def base_scope_satisfied(&block)
182
+ scope_category_satisfied(:base, &block)
183
+ end
184
+
185
+
186
+ # Overrides ActiveRecord::Base#scope to mark a scope as satisfying one or more categories if an extra option
187
+ # is passed:
188
+ #
189
+ # scope :foo, lambda { ... }, :satisfies => :deletion
190
+ #
191
+ # You can also pass an Array of category names to :satisfies.
192
+ def scope(name, body, *args, &block)
193
+ if args && args[-1] && args[-1].kind_of?(Hash)
194
+ opts = args.pop
195
+
196
+ categories = Array(opts.delete(:satisfies)).compact
197
+
198
+ if categories && categories.length > 0
199
+ if body.kind_of?(Proc)
200
+ # New, happy, dynamic scopes -- i.e., scope :foo, lambda { where(...) }
201
+ old_body = body
202
+ body = lambda do
203
+ out = old_body.call
204
+ out.scope_categories_satisfied!(categories)
205
+ out
206
+ end
207
+ else
208
+ # Old, sad, static scopes -- i.e., scope :foo, where(...)
209
+ body = body.clone
210
+ body.scope_categories_satisfied!(categories)
211
+ body
212
+ end
213
+ end
214
+
215
+ args.push(opts) if opts.size > 0
216
+ end
217
+
218
+ super(name, body, &block)
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
224
+
225
+ # When you call #includes to include an associated table, the Preloader ends up building a scope (via #build_scope)
226
+ # that it uses to retrieve these rows. We need to make sure we don't trigger any checks on this scope.
227
+ ::ActiveRecord::Associations::Preloader::Association.class_eval do
228
+ def build_scope_with_required_scopes_ignored
229
+ out = build_scope_without_required_scopes_ignored
230
+ out.all_scope_categories_satisfied!
231
+ out
232
+ end
233
+
234
+ alias_method_chain :build_scope, :required_scopes_ignored
235
+ end