activerecord-exclusive-arc 0.2.2 → 0.3.1
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/Gemfile +6 -3
- data/README.md +63 -40
- data/lib/exclusive_arc/model.rb +11 -8
- data/lib/exclusive_arc/version.rb +1 -1
- data/lib/generators/exclusive_arc_generator.rb +49 -11
- data/lib/generators/templates/migration.rb.erb +1 -0
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2fa6d4449542465ef3cc8420e9428a6faee1b4a91c547f5df660aeb615c15638
|
4
|
+
data.tar.gz: f945d9a76c78658e9c3a7a357f9c46202377ccd11488ec1cd286dfb467dd8eb2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8c829330cc1049ffddf223122cab10681e454f13184ea0ae7218bb55ed8f3b90e3ede9e140907917b2402f7a5740b0857ffe5c71dd0ed798a9a82cc364c30438
|
7
|
+
data.tar.gz: 140aea768b0e93d5e7cf22b7c0c6517a7ce19ecc64e48bce405501ed06adcc47072a8f789bccabb20fb9b0fb366b2928d5b14573bff9bed91245a4262fb360af
|
data/Gemfile
CHANGED
@@ -1,11 +1,14 @@
|
|
1
1
|
source "https://rubygems.org"
|
2
2
|
|
3
3
|
gem "debug"
|
4
|
-
|
5
|
-
gem "rails", "~> #{
|
6
|
-
|
4
|
+
if (rails_version = ENV["RAILS_VERSION"])
|
5
|
+
gem "rails", "~> #{rails_version}.0"
|
6
|
+
else
|
7
|
+
gem "rails"
|
8
|
+
end
|
7
9
|
gem "pg"
|
8
10
|
gem "sqlite3"
|
9
11
|
gem "standard", "~> 1.26"
|
12
|
+
gem "mysql2"
|
10
13
|
|
11
14
|
gemspec
|
data/README.md
CHANGED
@@ -1,31 +1,22 @@
|
|
1
|
-
|
1
|
+
## 💫 `activerecord-exclusive-arc` 💫
|
2
2
|
|
3
|
-
|
4
|
-
models.
|
3
|
+
A RubyGem that allows an ActiveRecord model to exclusively belong to one of any number of different
|
4
|
+
types of ActiveRecord models.
|
5
5
|
|
6
|
-
### Doesn’t Rails already provide this?
|
6
|
+
### Doesn’t Rails already provide a way to do this?
|
7
7
|
|
8
|
-
|
9
|
-
fact that the Ruby class name is stored in the database as a string. If you want to change the name of the
|
10
|
-
Ruby class used for such reasons, you must also update the database strings that represent it. The bleeding
|
11
|
-
of application-layer definitions into the database may become a liability.
|
12
|
-
|
13
|
-
Another common argument concerns referential integrity. _Foreign Key Constraints_ are a common mechanism to
|
14
|
-
ensure primary keys of tables can be reliably used as foreign keys on others. This becomes harder to enforce
|
15
|
-
when a column that represents a Ruby class is one of the components required for unique identification.
|
16
|
-
|
17
|
-
There are also quality of life considerations, such as not being able to eager-load the `belongs_to ...
|
18
|
-
polymorphic: true` relationship and the fact that polymorphic indexes require multiple columns.
|
8
|
+
Yes but [here’s a post about why this exists](https://waymondo.com/posts/are-exclusive-arcs-evil/).
|
19
9
|
|
20
10
|
### So how does this work?
|
21
11
|
|
22
|
-
It reduces the boilerplate of managing a _Polymorphic Assication_ modeled as a pattern called an
|
23
|
-
Arc_
|
24
|
-
|
12
|
+
It reduces the boilerplate of managing a _Polymorphic Assication_ modeled as a pattern called an
|
13
|
+
_Exclusive Arc_, where each potential polymorphic reference has its own foreign key. This maps
|
14
|
+
nicely to a set of optional `belongs_to` relationships, some polymorphic convenience methods, and a
|
15
|
+
database check constraint with a matching `ActiveRecord` validation.
|
25
16
|
|
26
17
|
## How to use
|
27
18
|
|
28
|
-
Firstly,
|
19
|
+
Firstly, add the gem to your `Gemfile` and `bundle install`:
|
29
20
|
|
30
21
|
```ruby
|
31
22
|
gem "activerecord-exclusive-arc"
|
@@ -37,8 +28,8 @@ The feature set of this gem is offered via a Rails generator command:
|
|
37
28
|
bin/rails g exclusive_arc <Model> <arc> <belongs_to1> <belongs_to2> ...
|
38
29
|
```
|
39
30
|
|
40
|
-
This assumes you already have a `<Model>`. The `<arc>` is the name of the polymorphic association
|
41
|
-
establish that may either be a `<belongs_to1>`, `<belongs_to2>`, etc. Say we ran:
|
31
|
+
This assumes you already have a `<Model>`. The `<arc>` is the name of the polymorphic association
|
32
|
+
you want to establish that may either be a `<belongs_to1>`, `<belongs_to2>`, etc. Say we ran:
|
42
33
|
|
43
34
|
```
|
44
35
|
bin/rails g exclusive_arc Comment commentable post comment
|
@@ -84,25 +75,26 @@ class Comment < ApplicationRecord
|
|
84
75
|
end
|
85
76
|
```
|
86
77
|
|
87
|
-
Continuing with our example, the generator command would also produce a migration that looks like
|
78
|
+
Continuing with our example, the generator command would also produce a migration that looks like
|
79
|
+
this:
|
88
80
|
|
89
81
|
```ruby
|
90
|
-
class
|
82
|
+
class CommentCommentableExclusiveArcPostComment < ActiveRecord::Migration[7.0]
|
91
83
|
def change
|
92
84
|
add_reference :comments, :post, foreign_key: true, index: {where: "post_id IS NOT NULL"}
|
93
85
|
add_reference :comments, :comment, foreign_key: true, index: {where: "comment_id IS NOT NULL"}
|
94
86
|
add_check_constraint(
|
95
87
|
:comments,
|
96
88
|
"(CASE WHEN post_id IS NULL THEN 0 ELSE 1 END + CASE WHEN comment_id IS NULL THEN 0 ELSE 1 END) = 1",
|
97
|
-
name:
|
89
|
+
name: "commentable"
|
98
90
|
)
|
99
91
|
end
|
100
92
|
end
|
101
93
|
```
|
102
94
|
|
103
|
-
The check constraint ensures `ActiveRecord` validations can’t be bypassed to break the fabeled
|
104
|
-
Can Only Be One
|
105
|
-
lookup performance for each individual polymorphic assoication.
|
95
|
+
The check constraint ensures `ActiveRecord` validations can’t be bypassed to break the fabeled
|
96
|
+
rule - "There Can Only Be One️". Traditional foreign key constraints can be used and the partial
|
97
|
+
indexes provide improved lookup performance for each individual polymorphic assoication.
|
106
98
|
|
107
99
|
### Exclusive Arc Options
|
108
100
|
|
@@ -114,30 +106,61 @@ Usage:
|
|
114
106
|
rails generate exclusive_arc NAME [arc belongs_to1 belongs_to2 ...] [options]
|
115
107
|
|
116
108
|
Options:
|
117
|
-
[--skip-namespace], [--no-skip-namespace] # Skip namespace (affects only isolated engines)
|
118
|
-
[--skip-collision-check], [--no-skip-collision-check] # Skip collision check
|
119
109
|
[--optional], [--no-optional] # Exclusive arc is optional
|
120
110
|
[--skip-foreign-key-constraints], [--no-skip-foreign-key-constraints] # Skip foreign key constraints
|
121
111
|
[--skip-foreign-key-indexes], [--no-skip-foreign-key-indexes] # Skip foreign key partial indexes
|
122
112
|
[--skip-check-constraint], [--no-skip-check-constraint] # Skip check constraint
|
123
113
|
|
124
|
-
Runtime options:
|
125
|
-
-f, [--force] # Overwrite files that already exist
|
126
|
-
-p, [--pretend], [--no-pretend] # Run but do not make any changes
|
127
|
-
-q, [--quiet], [--no-quiet] # Suppress status output
|
128
|
-
-s, [--skip], [--no-skip] # Skip files that already exist
|
129
|
-
|
130
114
|
Adds an Exclusive Arc to an ActiveRecord model and generates the migration for it
|
131
115
|
```
|
132
116
|
|
133
|
-
Notably, if you want to make an Exclusive Arc optional, you can use the `--optional` flag. This will
|
134
|
-
the definition in your `ActiveRecord` model and loosen both the validation and database check
|
135
|
-
that there can be 0 or 1 foreign keys set for the polymorphic
|
117
|
+
Notably, if you want to make an Exclusive Arc optional, you can use the `--optional` flag. This will
|
118
|
+
adjust the definition in your `ActiveRecord` model and loosen both the validation and database check
|
119
|
+
constraint so that there can be 0 or 1 foreign keys set for the polymorphic reference.
|
120
|
+
|
121
|
+
### Updating an existing exclusive arc
|
122
|
+
|
123
|
+
If you need to add an additional polymorphic option to an existing exclusive arc, you can simply run
|
124
|
+
the generator command again with the additional target. Existing references will be skipped and the
|
125
|
+
check constraint will be removed and re-added in a reversible manner.
|
126
|
+
|
127
|
+
```
|
128
|
+
bin/rails g exclusive_arc Comment commentable post comment page
|
129
|
+
```
|
130
|
+
|
131
|
+
``` ruby
|
132
|
+
class CommentCommentableExclusiveArcPostCommentPage < ActiveRecord::Migration[7.0]
|
133
|
+
def change
|
134
|
+
add_reference :comments, :page, foreign_key: true, index: {where: "page_id IS NOT NULL"}
|
135
|
+
remove_check_constraint(
|
136
|
+
:comments,
|
137
|
+
"(CASE WHEN post_id IS NULL THEN 0 ELSE 1 END + CASE WHEN comment_id IS NULL THEN 0 ELSE 1 END) = 1",
|
138
|
+
name: "commentable"
|
139
|
+
)
|
140
|
+
add_check_constraint(
|
141
|
+
:comments,
|
142
|
+
"(CASE WHEN post_id IS NULL THEN 0 ELSE 1 END + CASE WHEN comment_id IS NULL THEN 0 ELSE 1 END + CASE WHEN page_id IS NULL THEN 0 ELSE 1 END) = 1",
|
143
|
+
name: "commentable"
|
144
|
+
)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
```
|
148
|
+
|
149
|
+
The registration in the model will be updated as well.
|
150
|
+
|
151
|
+
``` ruby
|
152
|
+
class Comment < ApplicationRecord
|
153
|
+
include ExclusiveArc::Model
|
154
|
+
has_exclusive_arc :commentable, [:post, :comment, :page]
|
155
|
+
end
|
156
|
+
```
|
136
157
|
|
137
158
|
### Compatibility
|
138
159
|
|
139
|
-
Currently `activerecord-exclusive-arc` is tested against a matrix of
|
140
|
-
|
160
|
+
Currently `activerecord-exclusive-arc` is tested against a matrix of:
|
161
|
+
* Ruby 2.7 and 3.3
|
162
|
+
* Rails 6.1, 7.0, 7.1
|
163
|
+
* `postgresql`, `sqlite3`, and `mysql2` database adapters
|
141
164
|
|
142
165
|
### Contributing
|
143
166
|
|
data/lib/exclusive_arc/model.rb
CHANGED
@@ -15,7 +15,9 @@ module ExclusiveArc
|
|
15
15
|
next if reflections[option.to_s]
|
16
16
|
|
17
17
|
belongs_to(option, optional: true)
|
18
|
+
validate :"validate_#{arc}"
|
18
19
|
end
|
20
|
+
|
19
21
|
exclusive_arcs[arc] = Definition.new(
|
20
22
|
reflections: reflections.slice(*belong_tos.map(&:to_s)),
|
21
23
|
options: args[2] || {}
|
@@ -39,9 +41,11 @@ module ExclusiveArc
|
|
39
41
|
assign_exclusive_arc(:#{arc}, polymorphic)
|
40
42
|
@#{arc} = polymorphic
|
41
43
|
end
|
42
|
-
RUBY
|
43
44
|
|
44
|
-
|
45
|
+
def validate_#{arc}
|
46
|
+
validate_exclusive_arc(:#{arc})
|
47
|
+
end
|
48
|
+
RUBY
|
45
49
|
end
|
46
50
|
end
|
47
51
|
|
@@ -54,12 +58,11 @@ module ExclusiveArc
|
|
54
58
|
assign_attributes attributes
|
55
59
|
end
|
56
60
|
|
57
|
-
def
|
58
|
-
exclusive_arcs.
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
end
|
61
|
+
def validate_exclusive_arc(arc)
|
62
|
+
definition = exclusive_arcs.fetch(arc)
|
63
|
+
foreign_key_count = definition.reflections.keys.count { |name| !!public_send(name) }
|
64
|
+
valid = definition.options[:optional] ? foreign_key_count.in?([0, 1]) : foreign_key_count == 1
|
65
|
+
errors.add(arc, :arc_not_exclusive) unless valid
|
63
66
|
end
|
64
67
|
end
|
65
68
|
end
|
@@ -31,15 +31,20 @@ class ExclusiveArcGenerator < ActiveRecord::Generators::Base
|
|
31
31
|
model_file_path,
|
32
32
|
class_name.demodulize,
|
33
33
|
<<~RB
|
34
|
-
#{indents}
|
34
|
+
#{indents}include ExclusiveArc::Model
|
35
35
|
RB
|
36
36
|
)
|
37
|
-
|
37
|
+
gsub_file(
|
38
38
|
model_file_path,
|
39
|
-
|
40
|
-
|
41
|
-
|
39
|
+
/has_exclusive_arc :#{arc}(.*)$/,
|
40
|
+
""
|
41
|
+
)
|
42
|
+
inject_into_file(
|
43
|
+
model_file_path,
|
44
|
+
<<~RB,
|
45
|
+
#{indents}has_exclusive_arc #{model_exclusive_arcs}
|
42
46
|
RB
|
47
|
+
after: "include ExclusiveArc::Model\n"
|
43
48
|
)
|
44
49
|
end
|
45
50
|
|
@@ -52,8 +57,8 @@ class ExclusiveArcGenerator < ActiveRecord::Generators::Base
|
|
52
57
|
|
53
58
|
def add_references
|
54
59
|
belong_tos.map do |reference|
|
55
|
-
add_reference(reference)
|
56
|
-
end.join("\n")
|
60
|
+
add_reference(reference) unless column_exists?(reference)
|
61
|
+
end.compact.join("\n")
|
57
62
|
end
|
58
63
|
|
59
64
|
def add_reference(reference)
|
@@ -78,11 +83,22 @@ class ExclusiveArcGenerator < ActiveRecord::Generators::Base
|
|
78
83
|
end
|
79
84
|
|
80
85
|
def migration_file_name
|
81
|
-
"#{class_name.delete(":").underscore}_#{arc}
|
86
|
+
"#{class_name.delete(":").underscore}_#{arc}_exclusive_arc_#{belong_tos.map(&:underscore).join("_")}.rb"
|
82
87
|
end
|
83
88
|
|
84
89
|
def migration_class_name
|
85
|
-
|
90
|
+
(
|
91
|
+
[class_name.delete(":").singularize, arc.classify, "ExclusiveArc"] |
|
92
|
+
belong_tos.map(&:classify)
|
93
|
+
).join
|
94
|
+
end
|
95
|
+
|
96
|
+
def existing_check_constraint
|
97
|
+
@existing_check_constraint ||=
|
98
|
+
class_name.constantize.connection.check_constraints(class_name.constantize.table_name)
|
99
|
+
.find { |constraint| constraint.name == arc }
|
100
|
+
rescue
|
101
|
+
nil
|
86
102
|
end
|
87
103
|
|
88
104
|
def reference_type(reference)
|
@@ -98,19 +114,41 @@ class ExclusiveArcGenerator < ActiveRecord::Generators::Base
|
|
98
114
|
reference.tableize
|
99
115
|
end
|
100
116
|
|
117
|
+
def column_exists?(reference)
|
118
|
+
foreign_key = foreign_key_name(reference)
|
119
|
+
class_name.constantize.column_names.include?(foreign_key)
|
120
|
+
rescue
|
121
|
+
false
|
122
|
+
end
|
123
|
+
|
101
124
|
def foreign_key_name(reference)
|
102
125
|
class_name.constantize.reflections[reference].foreign_key
|
103
126
|
rescue
|
104
127
|
"#{reference}_id"
|
105
128
|
end
|
106
129
|
|
130
|
+
def remove_check_constraint
|
131
|
+
return unless class_name.constantize.connection.supports_check_constraints?
|
132
|
+
return if options[:skip_check_constraint]
|
133
|
+
return unless existing_check_constraint&.expression
|
134
|
+
|
135
|
+
<<-RUBY.chomp
|
136
|
+
remove_check_constraint(
|
137
|
+
:#{table_name},
|
138
|
+
"#{existing_check_constraint.expression.squish}",
|
139
|
+
name: "#{arc}"
|
140
|
+
)
|
141
|
+
RUBY
|
142
|
+
end
|
143
|
+
|
107
144
|
def add_check_constraint
|
145
|
+
return unless class_name.constantize.connection.supports_check_constraints?
|
108
146
|
return if options[:skip_check_constraint]
|
109
|
-
<<-RUBY
|
147
|
+
<<-RUBY.chomp
|
110
148
|
add_check_constraint(
|
111
149
|
:#{table_name},
|
112
150
|
"#{check_constraint}",
|
113
|
-
name:
|
151
|
+
name: "#{arc}"
|
114
152
|
)
|
115
153
|
RUBY
|
116
154
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-exclusive-arc
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- justin talbott
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-01-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -89,7 +89,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
89
89
|
- !ruby/object:Gem::Version
|
90
90
|
version: '0'
|
91
91
|
requirements: []
|
92
|
-
rubygems_version: 3.4
|
92
|
+
rubygems_version: 3.5.4
|
93
93
|
signing_key:
|
94
94
|
specification_version: 4
|
95
95
|
summary: An ActiveRecord extension for polymorphic exclusive arc relationships
|