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 +7 -0
- data/.gitignore +18 -0
- data/.travis.yml +29 -0
- data/Gemfile +17 -0
- data/LICENSE.txt +22 -0
- data/README.md +277 -0
- data/Rakefile +6 -0
- data/lib/required_scopes.rb +17 -0
- data/lib/required_scopes/active_record/base.rb +235 -0
- data/lib/required_scopes/active_record/relation.rb +121 -0
- data/lib/required_scopes/active_record/version_compatibility.rb +157 -0
- data/lib/required_scopes/errors.rb +48 -0
- data/lib/required_scopes/version.rb +3 -0
- data/required_scopes.gemspec +56 -0
- data/spec/required_scopes/helpers/database_helper.rb +174 -0
- data/spec/required_scopes/helpers/system_helpers.rb +98 -0
- data/spec/required_scopes/system/associations_system_spec.rb +150 -0
- data/spec/required_scopes/system/base_scope_system_spec.rb +71 -0
- data/spec/required_scopes/system/basic_system_spec.rb +121 -0
- data/spec/required_scopes/system/inheritance_system_spec.rb +67 -0
- data/spec/required_scopes/system/methods_system_spec.rb +312 -0
- data/spec/required_scopes/system/static_scopes_system_spec.rb +31 -0
- metadata +141 -0
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
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 — 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
|
+
— 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: 
|
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 — `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,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
|