active_record-json_associations 0.6.8 → 0.9.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 +5 -5
- data/.gitignore +1 -0
- data/.travis.yml +15 -3
- data/Appraisals +21 -0
- data/README.md +14 -0
- data/active_record-json_associations.gemspec +3 -1
- data/bin/setup +8 -0
- data/gemfiles/rails_5.0.gemfile +8 -0
- data/gemfiles/rails_5.1.gemfile +7 -0
- data/gemfiles/rails_5.2.gemfile +7 -0
- data/gemfiles/rails_6.0.gemfile +7 -0
- data/gemfiles/rails_6.1.gemfile +7 -0
- data/lib/active_record/json_associations.rb +51 -8
- data/lib/active_record/json_associations/version.rb +1 -1
- data/spec/json_associations_spec.rb +149 -3
- data/spec/spec_helper.rb +1 -0
- metadata +44 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 82a8dd8639c2897e523652a7415754704227798f98e753681d0199cc70ec055d
|
4
|
+
data.tar.gz: bf59af54b968501e6b087a65ba7d95b5783fe5ec20c4c41182b5882bbdb9604d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1fbc06fdc2cd8dbf92bc5d336ab5d1b1dc45f393e26ae47e45573f346be04912544dee4b31097a3fa1767ce1981c05fa5eb046fd5428947afd9cfeb90cf9827c
|
7
|
+
data.tar.gz: 402f51353a4b4865e67ae982cc4794aa60658e2afc3e97c1c8faf3eb03f93ed0306020b4618dc870e62aa34c184d85dd3e424b647254bedaf5436b92a7af0199
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -1,6 +1,18 @@
|
|
1
1
|
language: ruby
|
2
2
|
rvm:
|
3
|
-
- 2.
|
4
|
-
- 2.
|
5
|
-
- 2.
|
3
|
+
- 2.4
|
4
|
+
- 2.5
|
5
|
+
- 2.6
|
6
|
+
- 2.7
|
7
|
+
gemfile:
|
8
|
+
- gemfiles/rails_5.0.gemfile
|
9
|
+
- gemfiles/rails_5.1.gemfile
|
10
|
+
- gemfiles/rails_5.2.gemfile
|
11
|
+
- gemfiles/rails_6.0.gemfile
|
12
|
+
jobs:
|
13
|
+
exclude:
|
14
|
+
- rvm: 2.4
|
15
|
+
gemfile: gemfiles/rails_6.0.gemfile
|
16
|
+
before_install:
|
17
|
+
- gem install bundler -v"~>1.17"
|
6
18
|
|
data/Appraisals
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
appraise "rails-5.0" do
|
2
|
+
gem "rails", "~>5.0.0"
|
3
|
+
gem "sqlite3", "~>1.3.13" # 1.4 seems to break rails 5.0?
|
4
|
+
end
|
5
|
+
|
6
|
+
appraise "rails-5.1" do
|
7
|
+
gem "rails", "~>5.1.0"
|
8
|
+
end
|
9
|
+
|
10
|
+
appraise "rails-5.2" do
|
11
|
+
gem "rails", "~>5.2.0"
|
12
|
+
end
|
13
|
+
|
14
|
+
appraise "rails-6.0" do
|
15
|
+
gem "rails", "~>6.0.0"
|
16
|
+
end
|
17
|
+
|
18
|
+
appraise "rails-6.1" do
|
19
|
+
gem "rails", "~>6.1.0"
|
20
|
+
end
|
21
|
+
|
data/README.md
CHANGED
@@ -37,6 +37,20 @@ parent.child_ids #=> [1,2]
|
|
37
37
|
parent.children? #=> true
|
38
38
|
```
|
39
39
|
|
40
|
+
And a scope method for finding records assocatied with an id:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
Parent.child_ids_including(2) # => [<Parent child_ids: [1,2,3]>]
|
44
|
+
```
|
45
|
+
|
46
|
+
Or any of specified array of ids:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
Parent.child_ids_including([2,4,5]) # => [<Parent child_ids: [1,2,3]>]
|
50
|
+
```
|
51
|
+
|
52
|
+
`touch: true` can be specified on belongs_to_many to touch the associated records' timestamps when the record is modified.
|
53
|
+
|
40
54
|
It also adds an `json_foreign_key` option to `has_many` for specifying that the foreign keys are in a json array.
|
41
55
|
|
42
56
|
```ruby
|
@@ -20,10 +20,12 @@ Gem::Specification.new do |spec|
|
|
20
20
|
|
21
21
|
spec.add_dependency "activerecord"
|
22
22
|
|
23
|
-
spec.add_development_dependency "bundler"
|
23
|
+
spec.add_development_dependency "bundler"
|
24
|
+
spec.add_development_dependency "appraisal"
|
24
25
|
spec.add_development_dependency "rake"
|
25
26
|
spec.add_development_dependency "rspec"
|
26
27
|
spec.add_development_dependency "sqlite3"
|
27
28
|
spec.add_development_dependency "byebug"
|
29
|
+
spec.add_development_dependency "timecop"
|
28
30
|
end
|
29
31
|
|
data/bin/setup
ADDED
@@ -3,7 +3,15 @@ require "json"
|
|
3
3
|
|
4
4
|
module ActiveRecord
|
5
5
|
module JsonAssociations
|
6
|
-
|
6
|
+
FIELD_INCLUDE_SCOPE_BUILDER_PROC = proc do |context, field, id|
|
7
|
+
context.where("#{field}='[#{id}]'").or(
|
8
|
+
context.where("#{field} LIKE '[#{id},%'")).or(
|
9
|
+
context.where("#{field} LIKE '%,#{id},%'")).or(
|
10
|
+
context.where("#{field} LIKE '%,#{id}]'"))
|
11
|
+
end
|
12
|
+
private_constant :FIELD_INCLUDE_SCOPE_BUILDER_PROC
|
13
|
+
|
14
|
+
def belongs_to_many(many, class_name: nil, touch: nil)
|
7
15
|
one = many.to_s.singularize
|
8
16
|
one_ids = :"#{one}_ids"
|
9
17
|
one_ids_equals = :"#{one_ids}="
|
@@ -14,9 +22,47 @@ module ActiveRecord
|
|
14
22
|
|
15
23
|
serialize one_ids, JSON
|
16
24
|
|
25
|
+
if touch
|
26
|
+
after_commit do
|
27
|
+
if respond_to?(:saved_changes)
|
28
|
+
old_ids, new_ids = saved_changes[one_ids.to_s]
|
29
|
+
else
|
30
|
+
old_ids, new_ids = previous_changes[one_ids.to_s]
|
31
|
+
end
|
32
|
+
ids = Array(old_ids) | Array(new_ids)
|
33
|
+
scope = class_name.constantize.where(self.class.primary_key => ids)
|
34
|
+
|
35
|
+
if scope.respond_to?(:touch) # AR 6.0+
|
36
|
+
scope.touch_all
|
37
|
+
elsif self.class.respond_to?(:touch_attributes_with_time) # AR 5.1+
|
38
|
+
scope.update_all self.class.touch_attributes_with_time
|
39
|
+
else # AR 5.0
|
40
|
+
attributes = timestamp_attributes_for_update_in_model.inject({}) do |attributes, key|
|
41
|
+
attributes.merge(key => current_time_from_proper_timezone)
|
42
|
+
end
|
43
|
+
scope.update_all attributes
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
extend Module.new {
|
49
|
+
define_method :"#{one_ids}_including" do |id|
|
50
|
+
raise "can't query for a record that does not have an id!" if id.blank?
|
51
|
+
if id.is_a?(Hash)
|
52
|
+
Array(id[:any]).inject(none) do |context, id|
|
53
|
+
context.or(FIELD_INCLUDE_SCOPE_BUILDER_PROC.call(self, one_ids, id))
|
54
|
+
end
|
55
|
+
else
|
56
|
+
FIELD_INCLUDE_SCOPE_BUILDER_PROC.call(self, one_ids, id)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
}
|
60
|
+
|
17
61
|
include Module.new {
|
18
62
|
define_method one_ids do
|
19
|
-
super()
|
63
|
+
super().tap do |value|
|
64
|
+
return send(one_ids_equals, []) if value.nil?
|
65
|
+
end
|
20
66
|
end
|
21
67
|
|
22
68
|
define_method one_ids_equals do |ids|
|
@@ -34,7 +80,7 @@ module ActiveRecord
|
|
34
80
|
fragments += ["#{klass.primary_key} NOT IN (#{ids.map(&:to_s).join(",")})"] if ids.any?
|
35
81
|
fragments += ids.reverse.map { |id| "#{klass.primary_key}=#{id}" }
|
36
82
|
order_by_ids = fragments.join(", ")
|
37
|
-
scope.order!(order_by_ids)
|
83
|
+
scope.order!(Arel.sql(order_by_ids))
|
38
84
|
end
|
39
85
|
|
40
86
|
define_method many_equals do |collection|
|
@@ -47,7 +93,7 @@ module ActiveRecord
|
|
47
93
|
}
|
48
94
|
end
|
49
95
|
|
50
|
-
def has_many many, scope = nil, options
|
96
|
+
def has_many many, scope = nil, **options, &extension
|
51
97
|
unless (scope.is_a?(Hash) && scope[:json_foreign_key]) || (options.is_a?(Hash) && options[:json_foreign_key])
|
52
98
|
return super
|
53
99
|
end
|
@@ -81,10 +127,7 @@ module ActiveRecord
|
|
81
127
|
|
82
128
|
define_method many do
|
83
129
|
klass = class_name.constantize
|
84
|
-
|
85
|
-
klass.where("#{foreign_key} LIKE '[#{id},%'")).or(
|
86
|
-
klass.where("#{foreign_key} LIKE '%,#{id},%'")).or(
|
87
|
-
klass.where("#{foreign_key} LIKE '%,#{id}]'"))
|
130
|
+
FIELD_INCLUDE_SCOPE_BUILDER_PROC.call(klass, foreign_key, id)
|
88
131
|
end
|
89
132
|
|
90
133
|
define_method many_equals do |collection|
|
@@ -9,16 +9,21 @@ describe ActiveRecord::JsonAssociations do
|
|
9
9
|
create_table :parents do |t|
|
10
10
|
t.text :child_ids
|
11
11
|
t.text :fuzzy_ids
|
12
|
+
t.timestamps
|
12
13
|
end
|
13
14
|
|
14
|
-
create_table :children
|
15
|
+
create_table :children do |t|
|
16
|
+
t.timestamps
|
17
|
+
end
|
15
18
|
|
16
|
-
create_table :pets
|
19
|
+
create_table :pets do |t|
|
20
|
+
t.timestamps
|
21
|
+
end
|
17
22
|
end
|
18
23
|
end
|
19
24
|
|
20
25
|
class Parent < ActiveRecord::Base
|
21
|
-
belongs_to_many :children
|
26
|
+
belongs_to_many :children, touch: true
|
22
27
|
belongs_to_many :fuzzies, class_name: "Pet"
|
23
28
|
end
|
24
29
|
|
@@ -38,6 +43,140 @@ describe ActiveRecord::JsonAssociations do
|
|
38
43
|
|
39
44
|
describe ".belongs_to_many :children" do
|
40
45
|
subject { Parent.new }
|
46
|
+
let!(:winner) { Parent.create! }
|
47
|
+
|
48
|
+
describe ".child_ids_including" do
|
49
|
+
context "finds records with the specified id" do
|
50
|
+
let(:child) { Child.create! }
|
51
|
+
|
52
|
+
it "as the whole json array" do
|
53
|
+
parent = Parent.create(children: [child])
|
54
|
+
expect(Parent.child_ids_including(child.id)).to eq [parent]
|
55
|
+
end
|
56
|
+
|
57
|
+
it "at the beginning of the json array" do
|
58
|
+
parent = Parent.create(children: [child, Child.create!])
|
59
|
+
expect(Parent.child_ids_including(child.id)).to eq [parent]
|
60
|
+
end
|
61
|
+
|
62
|
+
it "in the middle of the json array" do
|
63
|
+
parent = Parent.create(children: [Child.create!, child, Child.create!])
|
64
|
+
expect(Parent.child_ids_including(child.id)).to eq [parent]
|
65
|
+
end
|
66
|
+
|
67
|
+
it "at the end of the json array" do
|
68
|
+
parent = Parent.create(children: [Child.create!, child])
|
69
|
+
expect(Parent.child_ids_including(child.id)).to eq [parent]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context "finds records including any of the specified array of ids" do
|
74
|
+
let(:peter) { Child.create! }
|
75
|
+
let(:paul) { Child.create! }
|
76
|
+
|
77
|
+
it "both as the whole json array" do
|
78
|
+
parent = Parent.create(children: [peter, paul])
|
79
|
+
expect(Parent.child_ids_including(any: [peter.id, paul.id])).to eq [parent]
|
80
|
+
end
|
81
|
+
|
82
|
+
it "one as the whole json array" do
|
83
|
+
parent = Parent.create(children: [peter])
|
84
|
+
expect(Parent.child_ids_including(any: [peter.id, paul.id])).to eq [parent]
|
85
|
+
end
|
86
|
+
|
87
|
+
it "the other as the whole json array" do
|
88
|
+
parent = Parent.create(children: [paul])
|
89
|
+
expect(Parent.child_ids_including(any: [peter.id, paul.id])).to eq [parent]
|
90
|
+
end
|
91
|
+
|
92
|
+
it "both at the beginning of the json array" do
|
93
|
+
parent = Parent.create(children: [peter, paul, Child.create!])
|
94
|
+
expect(Parent.child_ids_including(any: [peter.id, paul.id])).to eq [parent]
|
95
|
+
end
|
96
|
+
|
97
|
+
it "one at the beginning of the json array" do
|
98
|
+
parent = Parent.create(children: [peter, Child.create!])
|
99
|
+
expect(Parent.child_ids_including(any: [peter.id, paul.id])).to eq [parent]
|
100
|
+
end
|
101
|
+
|
102
|
+
it "the other at the beginning of the json array" do
|
103
|
+
parent = Parent.create(children: [paul, Child.create!])
|
104
|
+
expect(Parent.child_ids_including(any: [peter.id, paul.id])).to eq [parent]
|
105
|
+
end
|
106
|
+
|
107
|
+
it "both in the middle of the json array" do
|
108
|
+
parent = Parent.create(children: [Child.create!, peter, paul, Child.create!])
|
109
|
+
expect(Parent.child_ids_including(any: [peter.id, paul.id])).to eq [parent]
|
110
|
+
end
|
111
|
+
|
112
|
+
it "one in the middle of the json array" do
|
113
|
+
parent = Parent.create(children: [Child.create!, peter, Child.create!])
|
114
|
+
expect(Parent.child_ids_including(any: [peter.id, paul.id])).to eq [parent]
|
115
|
+
end
|
116
|
+
|
117
|
+
it "the other in the middle of the json array" do
|
118
|
+
parent = Parent.create(children: [Child.create!, paul, Child.create!])
|
119
|
+
expect(Parent.child_ids_including(any: [peter.id, paul.id])).to eq [parent]
|
120
|
+
end
|
121
|
+
|
122
|
+
it "both at the end of the json array" do
|
123
|
+
parent = Parent.create(children: [Child.create!, peter, paul])
|
124
|
+
expect(Parent.child_ids_including(any: [peter.id, paul.id])).to eq [parent]
|
125
|
+
end
|
126
|
+
|
127
|
+
it "one at the end of the json array" do
|
128
|
+
parent = Parent.create(children: [Child.create!, peter])
|
129
|
+
expect(Parent.child_ids_including(any: [peter.id, paul.id])).to eq [parent]
|
130
|
+
end
|
131
|
+
|
132
|
+
it "the other at the end of the json array" do
|
133
|
+
parent = Parent.create(children: [Child.create!, paul])
|
134
|
+
expect(Parent.child_ids_including(any: [peter.id, paul.id])).to eq [parent]
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
describe "touch: true" do
|
140
|
+
around do |example|
|
141
|
+
old_zone = Time.zone
|
142
|
+
Time.zone = "UTC"
|
143
|
+
example.run
|
144
|
+
Time.zone = old_zone
|
145
|
+
end
|
146
|
+
|
147
|
+
let(:old_time) { 1.year.ago.round }
|
148
|
+
let(:new_time) { 1.second.ago.round }
|
149
|
+
|
150
|
+
around do |example|
|
151
|
+
Timecop.freeze(new_time) do
|
152
|
+
example.run
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
it "touches associated records" do
|
157
|
+
children = [Child.create!(updated_at: old_time), Child.create!(updated_at: old_time)]
|
158
|
+
fuzzies = [Pet.create!(updated_at: old_time), Pet.create!(updated_at: old_time)]
|
159
|
+
parent = Parent.create!(children: children, fuzzies: fuzzies)
|
160
|
+
expect(children.each(&:reload).map(&:updated_at)).to eq [new_time, new_time] # touch: true
|
161
|
+
expect(fuzzies.each(&:reload).map(&:updated_at)).to eq [old_time, old_time] # touch: nil
|
162
|
+
end
|
163
|
+
|
164
|
+
it "touches removed associated records" do
|
165
|
+
peter, paul, mary = Child.create!, Child.create!, Child.create!
|
166
|
+
parent = Parent.create!(children: [peter, paul, mary])
|
167
|
+
[peter, paul, mary].each { |child| child.update_column :updated_at, old_time }
|
168
|
+
parent.update!(children: [peter, paul])
|
169
|
+
expect([peter, paul, mary].each(&:reload).map(&:updated_at)).to eq [new_time, new_time, new_time]
|
170
|
+
end
|
171
|
+
|
172
|
+
it "touches added associated records" do
|
173
|
+
peter, paul, mary = Child.create!, Child.create!, Child.create!
|
174
|
+
parent = Parent.create!(children: [peter, paul])
|
175
|
+
[peter, paul, mary].each { |child| child.update_column :updated_at, old_time }
|
176
|
+
parent.update!(children: [peter, paul, mary])
|
177
|
+
expect([peter, paul, mary].each(&:reload).map(&:updated_at)).to eq [new_time, new_time, new_time]
|
178
|
+
end
|
179
|
+
end
|
41
180
|
|
42
181
|
describe "#child_ids" do
|
43
182
|
it "is empty by default" do
|
@@ -48,6 +187,13 @@ describe ActiveRecord::JsonAssociations do
|
|
48
187
|
subject.child_ids = [1,2,3]
|
49
188
|
expect(subject.child_ids).to eq [1,2,3]
|
50
189
|
end
|
190
|
+
|
191
|
+
it "can be pushed to" do
|
192
|
+
subject.child_ids << 1
|
193
|
+
subject.child_ids << 2
|
194
|
+
subject.child_ids << 3
|
195
|
+
expect(subject.child_ids).to eq [1,2,3]
|
196
|
+
end
|
51
197
|
end
|
52
198
|
|
53
199
|
describe "#child_ids=" do
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_record-json_associations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Micah Geisel
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-01-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -28,16 +28,30 @@ dependencies:
|
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
32
39
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: appraisal
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
34
48
|
type: :development
|
35
49
|
prerelease: false
|
36
50
|
version_requirements: !ruby/object:Gem::Requirement
|
37
51
|
requirements:
|
38
|
-
- - "
|
52
|
+
- - ">="
|
39
53
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
54
|
+
version: '0'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: rake
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -94,21 +108,43 @@ dependencies:
|
|
94
108
|
- - ">="
|
95
109
|
- !ruby/object:Gem::Version
|
96
110
|
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: timecop
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
97
125
|
description: Instead of a many-to-many join table, serialize the ids into a JSON array.
|
98
126
|
email:
|
99
127
|
- micah@botandrose.com
|
100
|
-
executables:
|
128
|
+
executables:
|
129
|
+
- setup
|
101
130
|
extensions: []
|
102
131
|
extra_rdoc_files: []
|
103
132
|
files:
|
104
133
|
- ".gitignore"
|
105
134
|
- ".rspec"
|
106
135
|
- ".travis.yml"
|
136
|
+
- Appraisals
|
107
137
|
- Gemfile
|
108
138
|
- LICENSE.txt
|
109
139
|
- README.md
|
110
140
|
- Rakefile
|
111
141
|
- active_record-json_associations.gemspec
|
142
|
+
- bin/setup
|
143
|
+
- gemfiles/rails_5.0.gemfile
|
144
|
+
- gemfiles/rails_5.1.gemfile
|
145
|
+
- gemfiles/rails_5.2.gemfile
|
146
|
+
- gemfiles/rails_6.0.gemfile
|
147
|
+
- gemfiles/rails_6.1.gemfile
|
112
148
|
- lib/active_record/json_associations.rb
|
113
149
|
- lib/active_record/json_associations/version.rb
|
114
150
|
- spec/json_associations_spec.rb
|
@@ -132,8 +168,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
132
168
|
- !ruby/object:Gem::Version
|
133
169
|
version: '0'
|
134
170
|
requirements: []
|
135
|
-
|
136
|
-
rubygems_version: 2.4.8
|
171
|
+
rubygems_version: 3.0.3
|
137
172
|
signing_key:
|
138
173
|
specification_version: 4
|
139
174
|
summary: Instead of a many-to-many join table, serialize the ids into a JSON array.
|