required_scopes 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: ![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 — `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
|