active_record-json_associations 0.7.0 → 0.11.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da40cb0d7c64c3ae4fc7e90333a9ceb5687ed65676435f68edc66563e13f609c
4
- data.tar.gz: 77cd9023e9877aa3709162fbcfc1ac08c4ea5c7c581264fff450e20af58b6e2e
3
+ metadata.gz: 74ef04233cdf6b923c6be5762d8aa373428bf0ddd74a8fe48f46cbd9b3f16851
4
+ data.tar.gz: b40d4491c6619ecd3d858864706e3e05e459a3d3f3b20f50eadf0eed416c05db
5
5
  SHA512:
6
- metadata.gz: e219263a6d06ff6bc97dd86df3a289c82642f6b3c333779e107e8f21ae07c75fcad39f110cd84f7ef25303f752b43ee81b51c78a0d9f12591877cafddf861657
7
- data.tar.gz: 705383f050a1e3b84524c5a6dcc74e8e30027a3d663cd944e616da033527afd63f13bf4872c6b4c8175385939b240ceb6a0e4d4265622e53efa52e299831b3e2
6
+ metadata.gz: ea1b394dbe6ba8524b50228d9f226af19699171105cd8dda69b7f92f1b1fee6a9dfa06aa85301bfc2403fdb3831972799a38d01a6353c035235e1935fcc57bc2
7
+ data.tar.gz: e37aec9fff5cfe740cda139ed46c1b7fada912a30a26ebed3945ac6e0a8c7e9356071360278323867f949a666654cf47c9d351b2431c5a2b75fc568273b5439d
@@ -0,0 +1,28 @@
1
+ name: CI
2
+ on: [push, pull_request]
3
+ jobs:
4
+ test:
5
+ strategy:
6
+ fail-fast: false
7
+ matrix:
8
+ gemfile: [ rails_5.1, rails_5.2, rails_6.0, rails_6.1, rails_7.0 ]
9
+ ruby: [ 2.6, 2.7, '3.0' ]
10
+ exclude:
11
+ - gemfile: rails_5.1
12
+ ruby: 3.0
13
+ - gemfile: rails_5.2
14
+ ruby: 3.0
15
+ - gemfile: rails_7.0
16
+ ruby: 2.6
17
+
18
+ runs-on: ubuntu-latest
19
+ env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
20
+ BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile
21
+ steps:
22
+ - uses: actions/checkout@v2
23
+ - uses: ruby/setup-ruby@v1
24
+ with:
25
+ ruby-version: ${{ matrix.ruby }}
26
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
27
+ - run: bundle exec rake
28
+
data/Appraisals CHANGED
@@ -1,8 +1,3 @@
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
1
  appraise "rails-5.1" do
7
2
  gem "rails", "~>5.1.0"
8
3
  end
@@ -15,3 +10,11 @@ appraise "rails-6.0" do
15
10
  gem "rails", "~>6.0.0"
16
11
  end
17
12
 
13
+ appraise "rails-6.1" do
14
+ gem "rails", "~>6.1.0"
15
+ end
16
+
17
+ appraise "rails-7.0" do
18
+ gem "rails", "~>7.0.0"
19
+ end
20
+
data/README.md CHANGED
@@ -43,6 +43,14 @@ And a scope method for finding records assocatied with an id:
43
43
  Parent.child_ids_including(2) # => [<Parent child_ids: [1,2,3]>]
44
44
  ```
45
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
+
46
54
  It also adds an `json_foreign_key` option to `has_many` for specifying that the foreign keys are in a json array.
47
55
 
48
56
  ```ruby
@@ -55,6 +63,18 @@ parent = Parent.create children: [child]
55
63
  child.parents == [parent] #=> true
56
64
  ```
57
65
 
66
+ I can't figure out how to support building records off the association, so instead there are the `has_one`/`belongs_to` builder methods:
67
+
68
+ ```ruby
69
+ child.build_parent
70
+ child.create_parent
71
+ child.create_parent!
72
+
73
+ # also supports optional attributes:
74
+
75
+ child.build_parent(name: "Momma")
76
+ ```
77
+
58
78
  ## Requirements
59
79
 
60
80
  * ActiveRecord 5.0+
@@ -26,5 +26,6 @@ Gem::Specification.new do |spec|
26
26
  spec.add_development_dependency "rspec"
27
27
  spec.add_development_dependency "sqlite3"
28
28
  spec.add_development_dependency "byebug"
29
+ spec.add_development_dependency "timecop"
29
30
  end
30
31
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "rails", "~>5.0.0"
6
- gem "sqlite3", "~>1.3.13"
5
+ gem "rails", "~>6.1.0"
7
6
 
8
7
  gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~>7.0.0"
6
+
7
+ gemspec path: "../"
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module JsonAssociations
3
- VERSION = "0.7.0"
3
+ VERSION = "0.11.0"
4
4
  end
5
5
  end
@@ -11,7 +11,7 @@ module ActiveRecord
11
11
  end
12
12
  private_constant :FIELD_INCLUDE_SCOPE_BUILDER_PROC
13
13
 
14
- def belongs_to_many(many, class_name: nil)
14
+ def belongs_to_many(many, class_name: nil, touch: nil)
15
15
  one = many.to_s.singularize
16
16
  one_ids = :"#{one}_ids"
17
17
  one_ids_equals = :"#{one_ids}="
@@ -22,10 +22,38 @@ module ActiveRecord
22
22
 
23
23
  serialize one_ids, JSON
24
24
 
25
+ if touch
26
+ after_commit do
27
+ unless no_touching?
28
+ method = respond_to?(:saved_changes) ? :saved_changes : :previous_changes
29
+ old_ids, new_ids = send(method)[one_ids.to_s]
30
+ ids = Array(send(one_ids)) | Array(old_ids) | Array(new_ids)
31
+ scope = class_name.constantize.where(self.class.primary_key => ids)
32
+
33
+ if scope.respond_to?(:touch) # AR 6.0+
34
+ scope.touch_all
35
+ elsif self.class.respond_to?(:touch_attributes_with_time) # AR 5.1+
36
+ scope.update_all self.class.touch_attributes_with_time
37
+ else # AR 5.0
38
+ attributes = timestamp_attributes_for_update_in_model.inject({}) do |attributes, key|
39
+ attributes.merge(key => current_time_from_proper_timezone)
40
+ end
41
+ scope.update_all attributes
42
+ end
43
+ end
44
+ end
45
+ end
46
+
25
47
  extend Module.new {
26
48
  define_method :"#{one_ids}_including" do |id|
27
49
  raise "can't query for a record that does not have an id!" if id.blank?
28
- FIELD_INCLUDE_SCOPE_BUILDER_PROC.call(self, one_ids, id)
50
+ if id.is_a?(Hash)
51
+ Array(id[:any]).inject(none) do |context, id|
52
+ context.or(FIELD_INCLUDE_SCOPE_BUILDER_PROC.call(self, one_ids, id))
53
+ end
54
+ else
55
+ FIELD_INCLUDE_SCOPE_BUILDER_PROC.call(self, one_ids, id)
56
+ end
29
57
  end
30
58
  }
31
59
 
@@ -79,8 +107,12 @@ module ActiveRecord
79
107
  one_ids_equals = :"#{one_ids}="
80
108
  many_equals = :"#{many}="
81
109
  many_eh = :"#{many}?"
110
+ build_one = :"build_#{one}"
111
+ create_one = :"create_#{one}"
112
+ create_one_bang = :"create_#{one}!"
82
113
 
83
114
  class_name = options[:class_name] || one.classify
115
+ klass = class_name.constantize
84
116
 
85
117
  foreign_key = options[:json_foreign_key]
86
118
  foreign_key = :"#{model_name.singular}_ids" if foreign_key == true
@@ -91,13 +123,11 @@ module ActiveRecord
91
123
  end
92
124
 
93
125
  define_method one_ids_equals do |ids|
94
- klass = class_name.constantize
95
126
  normalized_ids = Array(ids).select(&:present?).map(&:to_i)
96
127
  send many_equals, klass.find(normalized_ids)
97
128
  end
98
129
 
99
130
  define_method many do
100
- klass = class_name.constantize
101
131
  FIELD_INCLUDE_SCOPE_BUILDER_PROC.call(klass, foreign_key, id)
102
132
  end
103
133
 
@@ -112,6 +142,18 @@ module ActiveRecord
112
142
  define_method many_eh do
113
143
  send(many).any?
114
144
  end
145
+
146
+ define_method build_one do |attributes={}|
147
+ klass.new attributes.merge!(foreign_key => [id])
148
+ end
149
+
150
+ define_method create_one do |attributes={}|
151
+ klass.create attributes.merge!(foreign_key => [id])
152
+ end
153
+
154
+ define_method create_one_bang do |attributes={}|
155
+ klass.create! attributes.merge!(foreign_key => [id])
156
+ end
115
157
  }
116
158
  end
117
159
  end
@@ -7,18 +7,24 @@ describe ActiveRecord::JsonAssociations do
7
7
  silence_stream(STDOUT) do
8
8
  ActiveRecord::Schema.define do
9
9
  create_table :parents do |t|
10
+ t.string :name
10
11
  t.text :child_ids
11
12
  t.text :fuzzy_ids
13
+ t.timestamps
12
14
  end
13
15
 
14
- create_table :children
16
+ create_table :children do |t|
17
+ t.timestamps
18
+ end
15
19
 
16
- create_table :pets
20
+ create_table :pets do |t|
21
+ t.timestamps
22
+ end
17
23
  end
18
24
  end
19
25
 
20
26
  class Parent < ActiveRecord::Base
21
- belongs_to_many :children
27
+ belongs_to_many :children, touch: true
22
28
  belongs_to_many :fuzzies, class_name: "Pet"
23
29
  end
24
30
 
@@ -38,6 +44,7 @@ describe ActiveRecord::JsonAssociations do
38
44
 
39
45
  describe ".belongs_to_many :children" do
40
46
  subject { Parent.new }
47
+ let!(:winner) { Parent.create! }
41
48
 
42
49
  describe ".child_ids_including" do
43
50
  context "finds records with the specified id" do
@@ -63,6 +70,129 @@ describe ActiveRecord::JsonAssociations do
63
70
  expect(Parent.child_ids_including(child.id)).to eq [parent]
64
71
  end
65
72
  end
73
+
74
+ context "finds records including any of the specified array of ids" do
75
+ let(:peter) { Child.create! }
76
+ let(:paul) { Child.create! }
77
+
78
+ it "both as the whole json array" do
79
+ parent = Parent.create(children: [peter, paul])
80
+ expect(Parent.child_ids_including(any: [peter.id, paul.id])).to eq [parent]
81
+ end
82
+
83
+ it "one as the whole json array" do
84
+ parent = Parent.create(children: [peter])
85
+ expect(Parent.child_ids_including(any: [peter.id, paul.id])).to eq [parent]
86
+ end
87
+
88
+ it "the other as the whole json array" do
89
+ parent = Parent.create(children: [paul])
90
+ expect(Parent.child_ids_including(any: [peter.id, paul.id])).to eq [parent]
91
+ end
92
+
93
+ it "both at the beginning of the json array" do
94
+ parent = Parent.create(children: [peter, paul, Child.create!])
95
+ expect(Parent.child_ids_including(any: [peter.id, paul.id])).to eq [parent]
96
+ end
97
+
98
+ it "one at the beginning of the json array" do
99
+ parent = Parent.create(children: [peter, Child.create!])
100
+ expect(Parent.child_ids_including(any: [peter.id, paul.id])).to eq [parent]
101
+ end
102
+
103
+ it "the other at the beginning of the json array" do
104
+ parent = Parent.create(children: [paul, Child.create!])
105
+ expect(Parent.child_ids_including(any: [peter.id, paul.id])).to eq [parent]
106
+ end
107
+
108
+ it "both in the middle of the json array" do
109
+ parent = Parent.create(children: [Child.create!, peter, paul, Child.create!])
110
+ expect(Parent.child_ids_including(any: [peter.id, paul.id])).to eq [parent]
111
+ end
112
+
113
+ it "one in the middle of the json array" do
114
+ parent = Parent.create(children: [Child.create!, peter, Child.create!])
115
+ expect(Parent.child_ids_including(any: [peter.id, paul.id])).to eq [parent]
116
+ end
117
+
118
+ it "the other in the middle of the json array" do
119
+ parent = Parent.create(children: [Child.create!, paul, Child.create!])
120
+ expect(Parent.child_ids_including(any: [peter.id, paul.id])).to eq [parent]
121
+ end
122
+
123
+ it "both at the end of the json array" do
124
+ parent = Parent.create(children: [Child.create!, peter, paul])
125
+ expect(Parent.child_ids_including(any: [peter.id, paul.id])).to eq [parent]
126
+ end
127
+
128
+ it "one at the end of the json array" do
129
+ parent = Parent.create(children: [Child.create!, peter])
130
+ expect(Parent.child_ids_including(any: [peter.id, paul.id])).to eq [parent]
131
+ end
132
+
133
+ it "the other at the end of the json array" do
134
+ parent = Parent.create(children: [Child.create!, paul])
135
+ expect(Parent.child_ids_including(any: [peter.id, paul.id])).to eq [parent]
136
+ end
137
+ end
138
+ end
139
+
140
+ describe "touch: true" do
141
+ around do |example|
142
+ old_zone = Time.zone
143
+ Time.zone = "UTC"
144
+ example.run
145
+ Time.zone = old_zone
146
+ end
147
+
148
+ let(:old_time) { 1.year.ago.round }
149
+ let(:new_time) { 1.second.ago.round }
150
+
151
+ around do |example|
152
+ Timecop.freeze(new_time) do
153
+ example.run
154
+ end
155
+ end
156
+
157
+ it "touches records associated upon creation" do
158
+ children = [Child.create!(updated_at: old_time), Child.create!(updated_at: old_time)]
159
+ fuzzies = [Pet.create!(updated_at: old_time), Pet.create!(updated_at: old_time)]
160
+ parent = Parent.create!(children: children, fuzzies: fuzzies)
161
+ expect(children.each(&:reload).map(&:updated_at)).to eq [new_time, new_time] # touch: true
162
+ expect(fuzzies.each(&:reload).map(&:updated_at)).to eq [old_time, old_time] # touch: nil
163
+ end
164
+
165
+ it "touches exising association records" do
166
+ children = [Child.create!, Child.create!]
167
+ parent = Parent.create!(children: children)
168
+ children.each { |child| child.update!(updated_at: old_time) }
169
+ parent.save!
170
+ expect(children.each(&:reload).map(&:updated_at)).to eq [new_time, new_time]
171
+ end
172
+
173
+ it "touches removed associated records" do
174
+ peter, paul, mary = Child.create!, Child.create!, Child.create!
175
+ parent = Parent.create!(children: [peter, paul, mary])
176
+ [peter, paul, mary].each { |child| child.update_column :updated_at, old_time }
177
+ parent.update!(children: [peter, paul])
178
+ expect([peter, paul, mary].each(&:reload).map(&:updated_at)).to eq [new_time, new_time, new_time]
179
+ end
180
+
181
+ it "touches added associated records" do
182
+ peter, paul, mary = Child.create!, Child.create!, Child.create!
183
+ parent = Parent.create!(children: [peter, paul])
184
+ [peter, paul, mary].each { |child| child.update_column :updated_at, old_time }
185
+ parent.update!(children: [peter, paul, mary])
186
+ expect([peter, paul, mary].each(&:reload).map(&:updated_at)).to eq [new_time, new_time, new_time]
187
+ end
188
+
189
+ it "skips touching if in a .no_touching block" do
190
+ children = [Child.create!, Child.create!]
191
+ parent = Parent.create!(children: children)
192
+ children.each { |child| child.update!(updated_at: old_time) }
193
+ ActiveRecord::Base.no_touching { parent.save! }
194
+ expect(children.each(&:reload).map(&:updated_at)).to eq [old_time, old_time]
195
+ end
66
196
  end
67
197
 
68
198
  describe "#child_ids" do
@@ -225,6 +355,67 @@ describe ActiveRecord::JsonAssociations do
225
355
  expect(subject.parents?).to be_truthy
226
356
  end
227
357
  end
358
+
359
+ describe "#build_parent" do
360
+ it "doesnt save the record" do
361
+ parent = subject.build_parent
362
+ expect(parent).to be_new_record
363
+ end
364
+
365
+ it "sets the foreign key column" do
366
+ parent = subject.build_parent
367
+ expect(parent.children).to eq([subject])
368
+ end
369
+
370
+ it "passes attributes through" do
371
+ parent = subject.build_parent(name: "Parent")
372
+ expect(parent.name).to eq("Parent")
373
+ end
374
+ end
375
+
376
+ describe "#create_parent" do
377
+ it "saves the record" do
378
+ parent = subject.create_parent
379
+ expect(parent).to be_persisted
380
+ end
381
+
382
+ it "sets the foreign key column" do
383
+ parent = subject.create_parent
384
+ expect(parent.children).to eq([subject])
385
+ end
386
+
387
+ it "passes attributes through" do
388
+ parent = subject.create_parent(name: "Parent")
389
+ expect(parent.name).to eq("Parent")
390
+ end
391
+
392
+ it "calls create on the model" do
393
+ expect(Parent).to receive(:create)
394
+ subject.create_parent
395
+ end
396
+ end
397
+
398
+ describe "#create_parent!" do
399
+ it "saves the record" do
400
+ parent = subject.create_parent!
401
+ expect(parent).to be_persisted
402
+ end
403
+
404
+ it "sets the foreign key column" do
405
+ parent = subject.create_parent!
406
+ expect(parent.children).to eq([subject])
407
+ end
408
+
409
+ it "passes attributes through" do
410
+ parent = subject.create_parent!(name: "Parent")
411
+ expect(parent.name).to eq("Parent")
412
+ end
413
+
414
+ it "calls create! on the model" do
415
+ expect(Parent).to receive(:create!)
416
+ subject.create_parent!
417
+ end
418
+ end
228
419
  end
229
420
 
230
421
  describe ".has_many :parents, json_foreign_key: :fuzzy_ids" do
data/spec/spec_helper.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require "byebug"
2
+ require "timecop"
2
3
 
3
4
  RSpec.configure do |config|
4
5
  config.filter_run focus: true
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.7.0
4
+ version: 0.11.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: 2020-02-27 00:00:00.000000000 Z
11
+ date: 2022-01-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -108,6 +108,20 @@ dependencies:
108
108
  - - ">="
109
109
  - !ruby/object:Gem::Version
110
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'
111
125
  description: Instead of a many-to-many join table, serialize the ids into a JSON array.
112
126
  email:
113
127
  - micah@botandrose.com
@@ -116,9 +130,9 @@ executables:
116
130
  extensions: []
117
131
  extra_rdoc_files: []
118
132
  files:
133
+ - ".github/workflows/ci.yml"
119
134
  - ".gitignore"
120
135
  - ".rspec"
121
- - ".travis.yml"
122
136
  - Appraisals
123
137
  - Gemfile
124
138
  - LICENSE.txt
@@ -126,10 +140,11 @@ files:
126
140
  - Rakefile
127
141
  - active_record-json_associations.gemspec
128
142
  - bin/setup
129
- - gemfiles/rails_5.0.gemfile
130
143
  - gemfiles/rails_5.1.gemfile
131
144
  - gemfiles/rails_5.2.gemfile
132
145
  - gemfiles/rails_6.0.gemfile
146
+ - gemfiles/rails_6.1.gemfile
147
+ - gemfiles/rails_7.0.gemfile
133
148
  - lib/active_record/json_associations.rb
134
149
  - lib/active_record/json_associations/version.rb
135
150
  - spec/json_associations_spec.rb
@@ -153,7 +168,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
153
168
  - !ruby/object:Gem::Version
154
169
  version: '0'
155
170
  requirements: []
156
- rubygems_version: 3.0.3
171
+ rubygems_version: 3.1.6
157
172
  signing_key:
158
173
  specification_version: 4
159
174
  summary: Instead of a many-to-many join table, serialize the ids into a JSON array.
data/.travis.yml DELETED
@@ -1,18 +0,0 @@
1
- language: ruby
2
- rvm:
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"
18
-