association_scope 0.3.3 → 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 +4 -4
- data/CHANGELOG.md +53 -0
- data/README.md +67 -2
- data/Rakefile +4 -5
- data/lib/association_scope/errors/polymorphic_association_error.rb +17 -0
- data/lib/association_scope/scope/belongs_to_reflection.rb +26 -31
- data/lib/association_scope/scope/has_and_belongs_to_many_reflection.rb +20 -11
- data/lib/association_scope/scope/has_many_reflection.rb +26 -10
- data/lib/association_scope/scope/through_reflection.rb +41 -11
- data/lib/association_scope/scope.rb +47 -0
- data/lib/association_scope/version.rb +1 -1
- data/lib/association_scope.rb +1 -1
- metadata +50 -20
- data/lib/association_scope/scope/has_one_reflection.rb +0 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0059e9dbe18df446d6d2e2052bfac18e55a2a63bc80cc3fdb8b1b7c1857de712'
|
|
4
|
+
data.tar.gz: 1cb46137b1e72f91cf131aa03961566deee85425e231cf68c9d8b47db4acdcdb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ad2173c80fc92258f0c559c5a4fd6314ed3c7fafe8ea1f9c50824df7434d8c130a828e3d2575786d31558d6d6f11b0b2583506f463d5283807e1d1f12ff13f81
|
|
7
|
+
data.tar.gz: 8ade7d8c45150524d87740fe69759199f0263054cc6184e1a93a5fba14acd5654df02d9967d1761b864b540d09be93aabd90b910a4c39b86b9664cbb16c3ed8e
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
## 1.0.0 - 2026-09-10
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Support is now limited to Ruby 3.2 through 4.0 and Rails 7.1 or newer.
|
|
10
|
+
- The development and test dependencies are pinned and the CI suite explicitly
|
|
11
|
+
tests the supported Ruby, Rails, and database adapter versions (SQLite and
|
|
12
|
+
PostgreSQL).
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Preserve scoped `belongs_to`, through, and HABTM association predicates.
|
|
17
|
+
- Support explicit source selections and self-referential `belongs_to` scopes.
|
|
18
|
+
- Honor custom association primary keys for direct associations.
|
|
19
|
+
- Return one record per owner for `has_one :through`, preserving source order
|
|
20
|
+
and applying offsets per owner for both direct and through `has_one` scopes.
|
|
21
|
+
- Exercise real queries and chaining in the installed-gem smoke test.
|
|
22
|
+
|
|
23
|
+
### Documentation
|
|
24
|
+
|
|
25
|
+
- Documented inverse-association requirements, generated scope naming,
|
|
26
|
+
unsupported options, upgrade notes, and verification commands.
|
|
27
|
+
|
|
28
|
+
### Upgrade notes
|
|
29
|
+
|
|
30
|
+
- Upgrade to Ruby 3.2 or newer and Rails 7.1 or newer before updating the gem.
|
|
31
|
+
- Replace polymorphic `belongs_to` association scopes with explicit application
|
|
32
|
+
scopes; they are not supported.
|
|
33
|
+
- Association scopes now require the corresponding inverse association on the
|
|
34
|
+
target model. Add `inverse_of` when Rails cannot infer that association.
|
|
35
|
+
- `has_association_scope_on` accepts only an association name or an array of
|
|
36
|
+
association names. It does not accept `only:` or `except:` options.
|
|
37
|
+
|
|
38
|
+
## 0.4.0 - 2021-11-12
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- Polymorphic association behavior and test coverage.
|
|
43
|
+
|
|
44
|
+
## 0.3.3 - 2021-11-12
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- Updated development dependencies and CI configuration.
|
|
49
|
+
|
|
50
|
+
## 0.3.0
|
|
51
|
+
|
|
52
|
+
See the [v0.3.0 release](https://github.com/datae95/association_scope/tree/v0.3.0)
|
|
53
|
+
for the changes in this release.
|
data/README.md
CHANGED
|
@@ -57,6 +57,40 @@ Topic.all.users
|
|
|
57
57
|
```
|
|
58
58
|
to retrieve the users of all of the topics of your application.
|
|
59
59
|
|
|
60
|
+
The argument must be an array of association names.
|
|
61
|
+
Each generated relation scope uses the pluralized association name: `:user`
|
|
62
|
+
creates `.users`, and `:profile` creates `.profiles`. Names must be unique
|
|
63
|
+
after pluralization and must not already be defined as class methods/scopes.
|
|
64
|
+
There is no `only:` or `except:` option; list exactly the associations to
|
|
65
|
+
expose.
|
|
66
|
+
|
|
67
|
+
### Inverse associations
|
|
68
|
+
|
|
69
|
+
The association must have a matching inverse association on its target model.
|
|
70
|
+
AssociationScope uses that inverse to construct the join. Define both sides
|
|
71
|
+
before calling `has_association_scope_on`, and use `inverse_of` when the inverse
|
|
72
|
+
cannot be inferred (for example, with a custom `class_name` or `source`):
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
class Topic < ApplicationRecord
|
|
76
|
+
belongs_to :user, inverse_of: :topics
|
|
77
|
+
has_association_scope_on [:user]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class User < ApplicationRecord
|
|
81
|
+
has_many :topics, inverse_of: :user
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Supported association types
|
|
86
|
+
|
|
87
|
+
`has_association_scope_on` supports `belongs_to`, `has_one`, `has_many`,
|
|
88
|
+
`has_many :through`, `has_one :through`, and `has_and_belongs_to_many`
|
|
89
|
+
associations. Polymorphic `has_many` associations (for example,
|
|
90
|
+
`has_many :pictures, as: :imageable`) are supported. A polymorphic
|
|
91
|
+
`belongs_to` association is not supported because its target model cannot be
|
|
92
|
+
determined when the scope is defined.
|
|
93
|
+
|
|
60
94
|
### Migration from `.of_model`
|
|
61
95
|
When you already use any form of `.of_model` scope, you can replace it with association scopes:
|
|
62
96
|
|
|
@@ -74,8 +108,8 @@ scope.of_users(users)
|
|
|
74
108
|
users.topics.merge(scope)
|
|
75
109
|
```
|
|
76
110
|
|
|
77
|
-
##
|
|
78
|
-
* This gem works with `reflections`.
|
|
111
|
+
## Limitations
|
|
112
|
+
* This gem works with Active Record `reflections`.
|
|
79
113
|
To make this work, the `has_association_scope_on` call has to be below your association definitions.
|
|
80
114
|
```ruby
|
|
81
115
|
# won't work
|
|
@@ -95,9 +129,40 @@ end
|
|
|
95
129
|
Workaround: Migrate JSON columns to JSONB.
|
|
96
130
|
* Error messages are not raised during application start, but on first instantiation, because of the order in which classes are loaded.
|
|
97
131
|
|
|
132
|
+
## Compatibility
|
|
133
|
+
|
|
134
|
+
The gem supports Ruby 3.2 through 4.0 and Rails 7.1 or newer. The CI matrix
|
|
135
|
+
tests Rails 7.1, 7.2, 8.0, and 8.1 on each supported Ruby version, using both
|
|
136
|
+
SQLite and PostgreSQL.
|
|
137
|
+
|
|
138
|
+
The test suite supports SQLite and PostgreSQL. PostgreSQL is recommended when
|
|
139
|
+
using `distinct` with JSON-valued columns; SQLite does not provide the same
|
|
140
|
+
JSON type behavior.
|
|
141
|
+
|
|
142
|
+
### Upgrade notes
|
|
143
|
+
|
|
144
|
+
Version 1.0 drops support for Ruby versions before 3.2 and Rails versions
|
|
145
|
+
before 7.1. Upgrade Ruby and Rails first, then run `bundle update
|
|
146
|
+
association_scope`. If you previously relied on a polymorphic `belongs_to`
|
|
147
|
+
association scope, replace it with an explicit application scope; that
|
|
148
|
+
association type is intentionally rejected. Also ensure every exposed
|
|
149
|
+
association has an inverse on its target model and remove unsupported
|
|
150
|
+
`only:`/`except:` arguments.
|
|
151
|
+
|
|
98
152
|
## Development
|
|
99
153
|
Clone this repository and run `bundle`.
|
|
154
|
+
|
|
155
|
+
The development Ruby version is 3.4.7. See [Compatibility](#compatibility) for
|
|
156
|
+
the supported runtime versions.
|
|
157
|
+
|
|
100
158
|
To use `rails console` you have to navigate to the dummy application
|
|
101
159
|
```bash
|
|
102
160
|
$ cd spec/dummy
|
|
103
161
|
```
|
|
162
|
+
|
|
163
|
+
### Verification
|
|
164
|
+
|
|
165
|
+
Run the test suite with `bundle exec rake` (or `bundle exec rspec`). Check
|
|
166
|
+
formatting with `bundle exec standardrb` and dependencies with
|
|
167
|
+
`bundle exec bundler-audit check --update`. The CI matrix additionally runs
|
|
168
|
+
the suite on the supported Ruby/Rails combinations with SQLite and PostgreSQL.
|
data/Rakefile
CHANGED
|
@@ -4,12 +4,11 @@ require "bundler/setup"
|
|
|
4
4
|
|
|
5
5
|
require "bundler/gem_tasks"
|
|
6
6
|
|
|
7
|
-
require "
|
|
7
|
+
require "rspec/core/rake_task"
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
t.
|
|
11
|
-
t.pattern = "test/**/*_test.rb"
|
|
9
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
|
10
|
+
t.pattern = "spec/**/*_spec.rb"
|
|
12
11
|
t.verbose = false
|
|
13
12
|
end
|
|
14
13
|
|
|
15
|
-
task default: :
|
|
14
|
+
task default: :spec
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AssociationScope
|
|
4
|
+
class PolymorphicAssociationError < StandardError
|
|
5
|
+
attr_accessor :association,
|
|
6
|
+
:model
|
|
7
|
+
|
|
8
|
+
def initialize(association: nil, model: nil)
|
|
9
|
+
@association = association
|
|
10
|
+
@model = model
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def message
|
|
14
|
+
"Association :#{association} is polymorph in #{model}!"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -4,44 +4,39 @@ module AssociationScope
|
|
|
4
4
|
class Scope
|
|
5
5
|
class BelongsToReflection < Scope
|
|
6
6
|
def apply
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
association_name = association.to_s.underscore.to_sym
|
|
11
|
-
own_table_name = class_name.to_s.pluralize.underscore
|
|
12
|
-
table_name = table_name class_name
|
|
13
|
-
|
|
14
|
-
model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
15
|
-
scope association.pluralize, -> do
|
|
16
|
-
if foreign_key.present?
|
|
17
|
-
class_name
|
|
18
|
-
.joins("JOIN #{table_name} ON #{table_name}.#{foreign_key} = #{own_table_name}.id")
|
|
19
|
-
else
|
|
20
|
-
class_name
|
|
21
|
-
.joins(table_name)
|
|
22
|
-
end
|
|
23
|
-
.where(table_name => { association_name =>
|
|
24
|
-
select("#{association_name}_id".to_sym) })
|
|
25
|
-
.distinct
|
|
26
|
-
end
|
|
27
|
-
RUBY
|
|
28
|
-
end
|
|
7
|
+
if reflection_details.options[:polymorphic]
|
|
8
|
+
raise PolymorphicAssociationError.new association: association, model: model
|
|
9
|
+
end
|
|
29
10
|
|
|
30
|
-
|
|
11
|
+
association = @association
|
|
12
|
+
class_name = reflection_details.klass
|
|
13
|
+
inverse_association = inverse_reflection(class_name)
|
|
31
14
|
|
|
32
|
-
|
|
33
|
-
case inverse_reflection(class_name)&.source_reflection&.class&.to_s&.split("::")&.last
|
|
34
|
-
when "HasOneReflection"
|
|
35
|
-
model.to_s.underscore.to_sym
|
|
36
|
-
when "HasManyReflection"
|
|
37
|
-
model.to_s.underscore.pluralize.to_sym
|
|
38
|
-
else
|
|
15
|
+
unless inverse_association
|
|
39
16
|
raise AssociationMissingError.new missing_in: class_name, association: model.to_s.underscore.pluralize
|
|
40
17
|
end
|
|
18
|
+
|
|
19
|
+
foreign_key = reflection_details.foreign_key
|
|
20
|
+
validate_scope!(reflection_details)
|
|
21
|
+
association_scope = reflection_details.scope
|
|
22
|
+
owner_key = model.arel_table[foreign_key]
|
|
23
|
+
target_key = class_name.arel_table[reflection_details.association_primary_key]
|
|
24
|
+
|
|
25
|
+
model.scope association.pluralize, -> do
|
|
26
|
+
Scope.target_relation(class_name, association_scope)
|
|
27
|
+
.where(target_key.in(reselect(owner_key).arel))
|
|
28
|
+
.distinct
|
|
29
|
+
end
|
|
41
30
|
end
|
|
42
31
|
|
|
32
|
+
private
|
|
33
|
+
|
|
43
34
|
def inverse_reflection(class_name)
|
|
44
|
-
|
|
35
|
+
inverse_name = reflection_details.options[:inverse_of]
|
|
36
|
+
|
|
37
|
+
class_name.reflections[inverse_name.to_s] ||
|
|
38
|
+
class_name.reflections[model.to_s.underscore.singularize] ||
|
|
39
|
+
class_name.reflections[model.to_s.underscore.pluralize]
|
|
45
40
|
end
|
|
46
41
|
|
|
47
42
|
def reflection_details
|
|
@@ -5,19 +5,20 @@ module AssociationScope
|
|
|
5
5
|
class HasAndBelongsToManyReflection < Scope
|
|
6
6
|
def apply
|
|
7
7
|
association = @association.pluralize
|
|
8
|
-
class_name = reflection_details.
|
|
9
|
-
|
|
8
|
+
class_name = reflection_details.klass
|
|
9
|
+
inverse_association = inverse_association(class_name)
|
|
10
|
+
validate_scope!(reflection_details)
|
|
11
|
+
association_scope = reflection_details.scope
|
|
12
|
+
owner_table = model.table_name
|
|
10
13
|
|
|
11
|
-
raise AssociationMissingError.new(missing_in: class_name, association: table_name) unless
|
|
14
|
+
raise AssociationMissingError.new(missing_in: class_name, association: model.table_name) unless inverse_association
|
|
12
15
|
|
|
13
|
-
model.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
end
|
|
20
|
-
RUBY
|
|
16
|
+
model.scope association, -> do
|
|
17
|
+
Scope.target_relation(class_name, association_scope)
|
|
18
|
+
.joins(inverse_association)
|
|
19
|
+
.where(owner_table => self)
|
|
20
|
+
.distinct
|
|
21
|
+
end
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
private
|
|
@@ -25,6 +26,14 @@ module AssociationScope
|
|
|
25
26
|
def reflection_details
|
|
26
27
|
model.reflections[association]
|
|
27
28
|
end
|
|
29
|
+
|
|
30
|
+
def inverse_association(class_name)
|
|
31
|
+
class_name.reflections.values.find do |reflection|
|
|
32
|
+
reflection.macro == :has_and_belongs_to_many &&
|
|
33
|
+
reflection.klass == model &&
|
|
34
|
+
reflection.join_table == reflection_details.join_table
|
|
35
|
+
end&.name
|
|
36
|
+
end
|
|
28
37
|
end
|
|
29
38
|
end
|
|
30
39
|
end
|
|
@@ -4,20 +4,30 @@ module AssociationScope
|
|
|
4
4
|
class Scope
|
|
5
5
|
class HasManyReflection < Scope
|
|
6
6
|
def apply
|
|
7
|
-
class_name = reflection_details.
|
|
7
|
+
class_name = reflection_details.klass
|
|
8
8
|
|
|
9
9
|
association = @association.pluralize
|
|
10
|
-
column_name = model.to_s.underscore
|
|
10
|
+
column_name = reflection_details.options[:as] || model.to_s.underscore
|
|
11
|
+
inverse_association = inverse_reflection(class_name, column_name)
|
|
11
12
|
|
|
12
|
-
raise AssociationMissingError.new(missing_in: class_name, association: column_name) unless
|
|
13
|
+
raise AssociationMissingError.new(missing_in: class_name, association: column_name) unless inverse_association
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
association_scope = reflection_details.scope
|
|
16
|
+
validate_scope!(reflection_details)
|
|
17
|
+
polymorphic = reflection_details.options[:as]
|
|
18
|
+
foreign_key = reflection_details.foreign_key
|
|
19
|
+
owner_key = model.arel_table[reflection_details.active_record_primary_key]
|
|
20
|
+
target_key = class_name.arel_table[foreign_key]
|
|
21
|
+
foreign_type = reflection_details.type if polymorphic
|
|
22
|
+
owner_type = model.polymorphic_name if polymorphic
|
|
23
|
+
singular = reflection_details.macro == :has_one
|
|
24
|
+
|
|
25
|
+
model.scope association, -> do
|
|
26
|
+
relation = Scope.target_relation(class_name, association_scope)
|
|
27
|
+
.where(target_key.in(reselect(owner_key).arel))
|
|
28
|
+
relation = relation.where(foreign_type => owner_type) if polymorphic
|
|
29
|
+
singular ? Scope.one_per_owner(relation, target_key) : relation.distinct
|
|
30
|
+
end
|
|
21
31
|
end
|
|
22
32
|
|
|
23
33
|
private
|
|
@@ -25,6 +35,12 @@ module AssociationScope
|
|
|
25
35
|
def reflection_details
|
|
26
36
|
model.reflections[@association]
|
|
27
37
|
end
|
|
38
|
+
|
|
39
|
+
def inverse_reflection(class_name, column_name)
|
|
40
|
+
inverse_name = reflection_details.options[:inverse_of]
|
|
41
|
+
|
|
42
|
+
class_name.reflections[inverse_name.to_s] || class_name.reflections[column_name.to_s]
|
|
43
|
+
end
|
|
28
44
|
end
|
|
29
45
|
end
|
|
30
46
|
end
|
|
@@ -5,19 +5,49 @@ module AssociationScope
|
|
|
5
5
|
class ThroughReflection < Scope
|
|
6
6
|
def apply
|
|
7
7
|
association = @association
|
|
8
|
-
class_name =
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
class_name = begin
|
|
9
|
+
reflection_details.klass
|
|
10
|
+
rescue ActiveRecord::AmbiguousSourceReflectionForThroughAssociation
|
|
11
|
+
# Rails cannot resolve an omitted source when multiple source
|
|
12
|
+
# reflections exist; retain the historical association-name
|
|
13
|
+
# fallback for this inherently ambiguous case.
|
|
14
|
+
association.singularize.camelize.constantize
|
|
15
|
+
end
|
|
16
|
+
source = begin
|
|
17
|
+
reflection_details.source_reflection
|
|
18
|
+
rescue ActiveRecord::AmbiguousSourceReflectionForThroughAssociation
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
validate_scope!(reflection_details)
|
|
22
|
+
source ||= reflection_details.through_reflection.klass.reflections.values.find do |candidate|
|
|
23
|
+
candidate.name.to_s == association.to_s && candidate.klass == class_name && candidate.through_reflection?
|
|
24
|
+
end
|
|
25
|
+
if source&.through_reflection?
|
|
26
|
+
first_join = source.options[:through]
|
|
27
|
+
second_join = reflection_details.options[:through].to_sym
|
|
28
|
+
else
|
|
29
|
+
first_join = inverse_reflection(class_name)&.options&.fetch(:through, nil) || inverse_reflection(class_name)&.options&.fetch(:source, nil)
|
|
30
|
+
second_join = compute_second_join class_name
|
|
31
|
+
end
|
|
11
32
|
|
|
12
33
|
raise AssociationMissingError.new missing_in: class_name, association: inverse unless inverse_reflection(class_name)
|
|
34
|
+
through_reflection = reflection_details.through_reflection
|
|
35
|
+
through_table = through_reflection.klass.table_name
|
|
36
|
+
through_key = through_reflection.belongs_to? ? through_reflection.association_primary_key : through_reflection.foreign_key
|
|
37
|
+
source_key = through_reflection.belongs_to? ? through_reflection.foreign_key : through_reflection.active_record_primary_key
|
|
38
|
+
owner_key = model.arel_table[source_key]
|
|
39
|
+
partition_key = through_reflection.klass.arel_table[through_key]
|
|
40
|
+
association_scope = reflection_details.scope
|
|
41
|
+
source_scope = source&.scope
|
|
42
|
+
validate_scope!(source) if source
|
|
43
|
+
singular = reflection_details.macro == :has_one
|
|
13
44
|
|
|
14
|
-
model.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
RUBY
|
|
45
|
+
model.scope association.pluralize, -> do
|
|
46
|
+
relation = Scope.target_relation(class_name, source_scope, association_scope)
|
|
47
|
+
.joins(first_join => second_join)
|
|
48
|
+
.where(through_table => {through_key => reselect(owner_key)})
|
|
49
|
+
singular ? Scope.one_per_owner(relation, partition_key) : relation.distinct
|
|
50
|
+
end
|
|
21
51
|
end
|
|
22
52
|
|
|
23
53
|
private
|
|
@@ -39,7 +69,7 @@ module AssociationScope
|
|
|
39
69
|
end
|
|
40
70
|
|
|
41
71
|
def compute_second_join class_name
|
|
42
|
-
if %w[HasOneReflection BelongsToReflection].include?(reflection_type
|
|
72
|
+
if %w[HasOneReflection BelongsToReflection].include?(reflection_type(class_name))
|
|
43
73
|
model.to_s.underscore.to_sym
|
|
44
74
|
else
|
|
45
75
|
model.to_s.underscore.pluralize.to_sym
|
|
@@ -10,11 +10,58 @@ module AssociationScope
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def self.inject_scopes(model, reflections)
|
|
13
|
+
unknown_association = reflections.find { |association| !model.reflections.key?(association) }
|
|
14
|
+
if unknown_association
|
|
15
|
+
raise AssociationMissingError.new(missing_in: model, association: unknown_association)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
generated_scopes = reflections.map(&:pluralize)
|
|
19
|
+
if generated_scopes.uniq.length != generated_scopes.length
|
|
20
|
+
raise ArgumentError, "association scope names must be unique after pluralization"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
generated_scopes.each do |scope_name|
|
|
24
|
+
if model.respond_to?(scope_name)
|
|
25
|
+
raise ArgumentError, "association scope :#{scope_name} is already defined on #{model}"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
13
29
|
model.reflections.slice(*reflections).each do |association, details|
|
|
14
30
|
scope_type = details.class.to_s.split("::").last
|
|
31
|
+
scope_type = "HasManyReflection" if scope_type == "HasOneReflection"
|
|
15
32
|
|
|
16
33
|
"AssociationScope::Scope::#{scope_type}".constantize.new(model, association).apply
|
|
17
34
|
end
|
|
18
35
|
end
|
|
36
|
+
|
|
37
|
+
def self.target_relation(klass, *scopes)
|
|
38
|
+
# Start independently of the owners' current scope, including for self joins.
|
|
39
|
+
scopes.compact.reduce(klass.default_scoped) do |relation, scope|
|
|
40
|
+
relation.instance_exec(&scope) || relation
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.one_per_owner(relation, owner_key)
|
|
45
|
+
klass = relation.klass
|
|
46
|
+
table = klass.arel_table
|
|
47
|
+
orders = relation.arel.orders.presence || [table[klass.primary_key].asc]
|
|
48
|
+
window = Arel::Nodes::Window.new.partition(owner_key).order(*orders)
|
|
49
|
+
row_number = Arel::Nodes::NamedFunction.new("ROW_NUMBER", []).over(window)
|
|
50
|
+
ranked = relation.except(:select, :order, :limit, :offset, :distinct)
|
|
51
|
+
.select(table[Arel.star], row_number.as("association_scope_row_number"))
|
|
52
|
+
|
|
53
|
+
# Keep ordering inside the window so offsets select a record for each owner.
|
|
54
|
+
result = klass.unscoped.from(ranked, klass.table_name)
|
|
55
|
+
.where(table[:association_scope_row_number].eq(relation.offset_value.to_i + 1))
|
|
56
|
+
.distinct
|
|
57
|
+
relation.select_values.any? ? result.select(*relation.select_values) : result
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def validate_scope!(reflection)
|
|
63
|
+
scope = reflection.scope
|
|
64
|
+
raise ArgumentError, "owner-dependent association scopes are not supported" if scope && scope.arity != 0
|
|
65
|
+
end
|
|
19
66
|
end
|
|
20
67
|
end
|
data/lib/association_scope.rb
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
require "association_scope/version"
|
|
4
4
|
require "association_scope/scope"
|
|
5
5
|
require "association_scope/scope/has_many_reflection"
|
|
6
|
-
require "association_scope/scope/has_one_reflection"
|
|
7
6
|
require "association_scope/scope/belongs_to_reflection"
|
|
8
7
|
require "association_scope/scope/has_and_belongs_to_many_reflection"
|
|
9
8
|
require "association_scope/scope/through_reflection"
|
|
10
9
|
|
|
11
10
|
require "association_scope/errors/association_missing_error"
|
|
11
|
+
require "association_scope/errors/polymorphic_association_error"
|
|
12
12
|
|
|
13
13
|
module AssociationScope
|
|
14
14
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: association_scope
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- datae
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: rails
|
|
@@ -16,83 +15,112 @@ dependencies:
|
|
|
16
15
|
requirements:
|
|
17
16
|
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
18
|
+
version: '7.1'
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - ">="
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
25
|
+
version: '7.1'
|
|
27
26
|
- !ruby/object:Gem::Dependency
|
|
28
27
|
name: standard
|
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
|
30
29
|
requirements:
|
|
31
30
|
- - "~>"
|
|
32
31
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: 1.
|
|
32
|
+
version: '1.56'
|
|
34
33
|
type: :development
|
|
35
34
|
prerelease: false
|
|
36
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
36
|
requirements:
|
|
38
37
|
- - "~>"
|
|
39
38
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: 1.
|
|
39
|
+
version: '1.56'
|
|
41
40
|
- !ruby/object:Gem::Dependency
|
|
42
41
|
name: yard
|
|
43
42
|
requirement: !ruby/object:Gem::Requirement
|
|
44
43
|
requirements:
|
|
45
44
|
- - "~>"
|
|
46
45
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: 0.9.
|
|
46
|
+
version: 0.9.37
|
|
48
47
|
type: :development
|
|
49
48
|
prerelease: false
|
|
50
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
50
|
requirements:
|
|
52
51
|
- - "~>"
|
|
53
52
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: 0.9.
|
|
53
|
+
version: 0.9.37
|
|
55
54
|
- !ruby/object:Gem::Dependency
|
|
56
55
|
name: rspec
|
|
57
56
|
requirement: !ruby/object:Gem::Requirement
|
|
58
57
|
requirements:
|
|
59
58
|
- - "~>"
|
|
60
59
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '3.
|
|
60
|
+
version: '3.13'
|
|
62
61
|
type: :development
|
|
63
62
|
prerelease: false
|
|
64
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
64
|
requirements:
|
|
66
65
|
- - "~>"
|
|
67
66
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '3.
|
|
69
|
-
|
|
67
|
+
version: '3.13'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: awesome_print
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: 1.9.2
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: 1.9.2
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: bundler-audit
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0.9'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0.9'
|
|
96
|
+
description: AssociationScope lets Active Record relations expose associations as
|
|
97
|
+
chainable scopes.
|
|
70
98
|
email:
|
|
71
99
|
- accounts@datae.de
|
|
72
100
|
executables: []
|
|
73
101
|
extensions: []
|
|
74
102
|
extra_rdoc_files: []
|
|
75
103
|
files:
|
|
104
|
+
- CHANGELOG.md
|
|
76
105
|
- MIT-LICENSE
|
|
77
106
|
- README.md
|
|
78
107
|
- Rakefile
|
|
79
108
|
- lib/association_scope.rb
|
|
80
109
|
- lib/association_scope/errors/association_missing_error.rb
|
|
110
|
+
- lib/association_scope/errors/polymorphic_association_error.rb
|
|
81
111
|
- lib/association_scope/scope.rb
|
|
82
112
|
- lib/association_scope/scope/belongs_to_reflection.rb
|
|
83
113
|
- lib/association_scope/scope/has_and_belongs_to_many_reflection.rb
|
|
84
114
|
- lib/association_scope/scope/has_many_reflection.rb
|
|
85
|
-
- lib/association_scope/scope/has_one_reflection.rb
|
|
86
115
|
- lib/association_scope/scope/through_reflection.rb
|
|
87
116
|
- lib/association_scope/version.rb
|
|
88
117
|
homepage: https://github.com/datae95/association_scope
|
|
89
118
|
licenses:
|
|
90
119
|
- MIT
|
|
91
120
|
metadata:
|
|
92
|
-
homepage_uri: https://github.com/datae95/association_scope
|
|
121
|
+
homepage_uri: https://github.com/datae95/association_scope#readme
|
|
93
122
|
source_code_uri: https://github.com/datae95/association_scope
|
|
94
|
-
changelog_uri: https://github.com/datae95/association_scope/
|
|
95
|
-
post_install_message:
|
|
123
|
+
changelog_uri: https://github.com/datae95/association_scope/blob/main/CHANGELOG.md
|
|
96
124
|
rdoc_options: []
|
|
97
125
|
require_paths:
|
|
98
126
|
- lib
|
|
@@ -100,15 +128,17 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
100
128
|
requirements:
|
|
101
129
|
- - ">="
|
|
102
130
|
- !ruby/object:Gem::Version
|
|
103
|
-
version: 2
|
|
131
|
+
version: '3.2'
|
|
132
|
+
- - "<"
|
|
133
|
+
- !ruby/object:Gem::Version
|
|
134
|
+
version: '4.1'
|
|
104
135
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
105
136
|
requirements:
|
|
106
137
|
- - ">="
|
|
107
138
|
- !ruby/object:Gem::Version
|
|
108
139
|
version: '0'
|
|
109
140
|
requirements: []
|
|
110
|
-
rubygems_version:
|
|
111
|
-
signing_key:
|
|
141
|
+
rubygems_version: 4.0.16
|
|
112
142
|
specification_version: 4
|
|
113
|
-
summary:
|
|
143
|
+
summary: Chain Active Record associations from relation scopes.
|
|
114
144
|
test_files: []
|