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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: b1d844089bbc9addaaa6680fff31aac70e44ecf8
4
- data.tar.gz: 53bacf65b6bbfe4a2e47cc300e88235733e7c20e
2
+ SHA256:
3
+ metadata.gz: 82a8dd8639c2897e523652a7415754704227798f98e753681d0199cc70ec055d
4
+ data.tar.gz: bf59af54b968501e6b087a65ba7d95b5783fe5ec20c4c41182b5882bbdb9604d
5
5
  SHA512:
6
- metadata.gz: 1e88b1f2079405a05c2b75490845f7c2dc2918110b3aebd977148c9b2f630a22bdec21ca276c75c3c2e20992a2e523b57961af054cc844caa8b27e57c1f7e723
7
- data.tar.gz: 98cc39387cfab3b919c44d44e0065d2f5b76fc2f24fc9d72053e6cadbd057f0524d12c744a4f77eac3d9ddcd0bb4153f533fd36682dd955085c8496d28a8a168
6
+ metadata.gz: 1fbc06fdc2cd8dbf92bc5d336ab5d1b1dc45f393e26ae47e45573f346be04912544dee4b31097a3fa1767ce1981c05fa5eb046fd5428947afd9cfeb90cf9827c
7
+ data.tar.gz: 402f51353a4b4865e67ae982cc4794aa60658e2afc3e97c1c8faf3eb03f93ed0306020b4618dc870e62aa34c184d85dd3e424b647254bedaf5436b92a7af0199
data/.gitignore CHANGED
@@ -6,6 +6,7 @@
6
6
  .ruby-version
7
7
  .ruby-gemset
8
8
  Gemfile.lock
9
+ gemfiles/*.lock
9
10
  InstalledFiles
10
11
  _yardoc
11
12
  coverage
@@ -1,6 +1,18 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.2.2
4
- - 2.3.3
5
- - 2.4.0
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
 
@@ -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", "~> 1.6"
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
 
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~>5.0.0"
6
+ gem "sqlite3", "~>1.3.13"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~>5.1.0"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~>5.2.0"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~>6.0.0"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~>6.1.0"
6
+
7
+ gemspec path: "../"
@@ -3,7 +3,15 @@ require "json"
3
3
 
4
4
  module ActiveRecord
5
5
  module JsonAssociations
6
- def belongs_to_many(many, class_name: nil)
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 = {}, &extension
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
- klass.where("#{foreign_key} LIKE '[#{id}]'").or(
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|
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module JsonAssociations
3
- VERSION = "0.6.8"
3
+ VERSION = "0.9.0"
4
4
  end
5
5
  end
@@ -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
@@ -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.6.8
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: 2017-05-02 00:00:00.000000000 Z
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: '1.6'
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: '1.6'
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
- rubyforge_project:
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.