quickery 0.1.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e91a2705cab309764e0f8a789d22462727389884c0a2e1aeed60b3a943f68bb1
4
- data.tar.gz: c8e6c59c8f35df9c27418de44e9f86eaadf24d793a469244408180d90348c528
3
+ metadata.gz: 20c5125563b48a48ba73907330199cf2b7d330e0e78bc3449db1ba92f29783aa
4
+ data.tar.gz: 047cc01e20747481b825ca1d4f26dfe34ba4ab31429a005be4b8767cf51b6e12
5
5
  SHA512:
6
- metadata.gz: 2450b89db93db6e347eda4e8fd688f70746fe7925c375f0901ac6406e47553261e511f836de2452052702b252f98757bcab6d40ae7f378d2a52166cd780a01a9
7
- data.tar.gz: 546af2185f0631a2f7e4533233e97ee0c04ab86171e01c6737bd250881d9f1805808646e8c4f234839c943d168dd62339c999e9c7ef49464f1c5764e2cef7861
6
+ metadata.gz: 7cdc80db27ccdbe249a2592bc0e537cd0cf4a8c2146afcc0a306ffadd01cb7a76e1181492d6e44795bd94862c23d7e1aadbaf42e19962f99f1ee4b2985ebdb21
7
+ data.tar.gz: 72f951760cd8737b117d1f11865cb2cc4c6b4ba5c427d86dcdffa704a2025d51432d45c69b0226eb0b2c42c20dd241ab93909dc8a83d8b4d51e8113db4eaeed8
data/README.md CHANGED
@@ -17,7 +17,7 @@
17
17
  1. Add the following to your `Gemfile`:
18
18
 
19
19
  ```ruby
20
- gem 'quickery', '~> 0.1'
20
+ gem 'quickery', '~> 1.0'
21
21
  ```
22
22
 
23
23
  2. Run:
@@ -37,22 +37,13 @@ class Employee < ApplicationRecord
37
37
 
38
38
  belongs_to :branch
39
39
 
40
- quickery do
41
- # TL;DR: the following line means:
42
- # make sure that this record's `branch_company_name` attribute will always have
43
- # the same value as branch.company.name and updates the value accordingly if it changes
44
- branch.company.name == :branch_company_name
45
-
46
- # feel free to rename :branch_company_name as you wish; it's just like any other attribute anyway
47
- # the == is a custom overloaded operator; it does not mean "is equal" but means "should equal to"
48
- # branch.company.name is a fluid expression that defines the attribute dependency
49
- # `branch` and `company` does not mean `branch` and `company` record
50
-
51
- # you may add more inside this quickery-block: i.e:
52
- # branch.name == :branch_name
53
- # branch.id == :branch_id
54
- # branch.company.country.name == :branch_company_country_name
55
- end
40
+ # TL;DR: the following line means:
41
+ # make sure that this record's `branch_company_name` attribute will always have the same value as
42
+ # branch.company.name and auto-updates the value if it (or any associated record in between) changes
43
+
44
+ quickery branch: { company: { name: :branch_company_name } }
45
+
46
+ # feel free to rename :branch_company_name as you wish; it's just like any other attribute anyway
56
47
  end
57
48
 
58
49
  # app/models/branch.rb
@@ -126,17 +117,15 @@ end
126
117
  # app/models/employee.rb
127
118
  class Employee < ApplicationRecord
128
119
  belongs_to :branch
129
- belongs_to :company, foreign_key: :branch_company_id
120
+ belongs_to :company, foreign_key: :branch_company_id, optional: true
130
121
 
131
- quickery do
132
- branch.company.id == :branch_company_id
133
- end
122
+ quickery { branch: { company: { id: :branch_company_id } } }
134
123
  end
135
124
  ```
136
125
 
137
126
  ```bash
138
127
  # bash
139
- rails generate migration add_branch_company_id_to_employees branch_company_id:bigint
128
+ rails generate migration add_branch_company_id_to_employees branch_company_id:bigint:index
140
129
  bundle exec rake db:migrate
141
130
  ```
142
131
 
@@ -149,31 +138,72 @@ employee = Employee.create!(branch: branch)
149
138
  puts employee.branch_company_id
150
139
  # => 1
151
140
 
141
+ puts employee.company
142
+ # => #<Company id: 1 name: 'Jollibee'>
143
+
152
144
  puts Employee.where(company: company)
153
145
  # => [#<Employee id: 1>]
154
146
 
155
147
  # as you may notice, the query above is a lot simpler and faster instead of doing it normally like below (if not using Quickery)
156
- # you may however still use belongs_to `:through` to achieve the simplified query like above, but it's still a lot slower because of JOINS
148
+ # you may however still use `has_many :through` to achieve a simplified code: `company.employees`, but it's still a lot slower because of JOINS
157
149
  puts Employee.joins(branch: :company).where(companies: { id: company.id })
158
150
  # => [#<Employee id: 1>]
159
151
  ```
160
152
 
153
+ ## Other Usage Examples
154
+
155
+ ```ruby
156
+ # app/models/employee.rb
157
+ class Employee < ApplicationRecord
158
+ # multiple-attributes and/or multiple-associations; as many, and as deep as you wish
159
+ quickery(
160
+ branch: {
161
+ name: :branch_name,
162
+ address: :branch_address,
163
+ company: {
164
+ name: :branch_company_name
165
+ }
166
+ },
167
+ user: {
168
+ first_name: :user_first_name,
169
+ last_name: :user_last_name
170
+ }
171
+ )
172
+ end
173
+ ```
174
+
175
+ ```ruby
176
+ # app/models/employee.rb
177
+ class Employee < ApplicationRecord
178
+ # `quickery` can be called multiple times
179
+ quickery { branch: { name: :branch_name } }
180
+ quickery { branch: { address: :branch_address } }
181
+ quickery { branch: { company: { name: :branch_company_name } } }
182
+ quickery { user: { first_name: :user_first_name } }
183
+ quickery { user: { last_name: :user_last_name } }
184
+ end
185
+ ```
186
+
187
+ ## Gotchas
188
+ * Quickery makes use of Rails model callbacks such as `before_save`. This meant that data-integrity holds unless `update_columns` or `update_column` is used which bypasses model callbacks, or unless any manual SQL update is performed.
189
+ * Quickery does not automatically update old records existing in the database that were created before you integrate Quickery, or before you add new/more Quickery-attributes for that model. One solution is [`recreate_quickery_cache!`](#recreate_quickery_cache) below.
190
+
161
191
  ## DSL
162
192
 
163
193
  ### For any subclass of `ActiveRecord::Base`:
164
194
 
195
+ * defines a set of "hidden" Quickery `before_create`, `before_update`, and `before_destroy` callbacks needed by Quickery to perform the "syncing" of attribute values
196
+
165
197
  #### Class Methods:
166
198
 
167
- ##### `quickery(&block)`
168
- * returns a `Quickery::AssociationBuilder` object
169
- * block is executed in the context of the `Quickery::AssociationBuilder` object,
170
- which means that you cannot access the model instance inside the block, as you are not supposed to.
171
- * inside the block you may define "quickery-defined attribute mappings";
172
- each mapping will create a `Quickery::QuickeryBuilder` object. i.e:
173
- * `branch.company.country.category.name == :branch_company_country_category_name`
174
- * You are required to specify `belongs_to :branch` association in this model.
199
+ ##### `quickery(mappings)`
200
+ * mappings (Hash)
201
+ * each mapping will create a `Quickery::QuickeryBuilder` object. i.e:
202
+ * `{ branch: { name: :branch_name }` will create one `Quickery::QuickeryBuilder`, while
203
+ * `{ branch: { name: :branch_name, id: :branch_id }` will create two `Quickery::QuickeryBuilder`
204
+ * In this particular example, you are required to specify `belongs_to :branch` in this model
175
205
  * Similarly, you are required to specify `belongs_to :company` inside `Branch` model, `belongs_to :country` inside `Company` model; etc...
176
- * each `Quickery::AssociationBuilder` defines a set of "hidden" `before_save`, `before_update`, `before_destroy`, and `before_create` callbacks across all models specified in the quickery-defined attribute association chain.
206
+ * defines a set of "hidden" Quickery `before_save`, `before_update`, `before_destroy`, and `before_create` callbacks across all models specified in the quickery-defined attribute association chain.
177
207
  * quickery-defined attributes such as say `:branch_company_country_category_name` are updated by Quickery automatically whenever any of it's dependent records across models have been changed. Note that updates in this way do not trigger model callbacks, as I wanted to isolate logic and scope of Quickery by not triggering model callbacks that you already have.
178
208
  * quickery-defined attributes such as say `:branch_company_country_category_name` are READ-only! Do not update these attributes manually. You can, but it will not automatically update the other end, and thus will break data integrity. If you want to re-update these attributes to match the other end, see `recreate_quickery_cache!` below.
179
209
 
@@ -207,7 +237,9 @@ puts Employee.joins(branch: :company).where(companies: { id: company.id })
207
237
 
208
238
  ## TODOs
209
239
  * Possibly support two-way mapping of attributes? So that you can do, say... `employee.update!(branch_company_name: 'somenewcompanyname')`
210
- * Improve "updates" across the quickery-defined model callbacks, by grouping attributes that will be updated and update in one go, instead of independently updating per each quickery-defined attribute
240
+ * Support `has_many` as currently only `belongs_to` is supported. This would then allow us to cache Array of values.
241
+ * Support custom-methods-values like [`persistize`](https://github.com/bebanjo/persistize), if it's easy enough to integrate something similar
242
+ * Support background-processing like in [`flattery`](https://github.com/evendis/flattery)
211
243
 
212
244
  ## Other Similar Gems
213
245
  See [my detailed comparisons](other_similar_gems_comparison.md)
@@ -232,6 +264,9 @@ See [my detailed comparisons](other_similar_gems_comparison.md)
232
264
  5. Create new Pull Request
233
265
 
234
266
  ## Changelog
267
+ * 1.0.0
268
+ * Done (TODO): DSL changed from quickery (block) into quickery (hash). Thanks to @xire28 and @sshaw_ in my [reddit post](https://www.reddit.com/r/ruby/comments/9dlcc5/i_just_published_a_new_gem_quickery_an/) for the suggestion.
269
+ * Done (TODO): Now updates in one go, instead of updating record per quickery-attribute, thereby greatly improving speed.
235
270
  * 0.1.4
236
271
  * add `railstie` as dependency to fix undefined constant error
237
272
  * 0.1.3
data/lib/quickery.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  Dir[__dir__ + '/quickery/*.rb'].each {|file| require file }
2
2
  Dir[__dir__ + '/quickery/active_record_extensions/*.rb'].each {|file| require file }
3
+ Dir[__dir__ + '/quickery/errors/*.rb'].each {|file| require file }
3
4
 
4
5
  module Quickery
5
6
  # Your code goes here...
@@ -0,0 +1,16 @@
1
+ require 'active_support'
2
+
3
+ module Quickery
4
+ module ActiveRecordExtensions
5
+ class << self
6
+ def included(base)
7
+ base.include Quickery::ActiveRecordExtensions::DSL
8
+ base.include Quickery::ActiveRecordExtensions::Callbacks
9
+ end
10
+ end
11
+ end
12
+ end
13
+
14
+ ActiveSupport.on_load(:active_record) do
15
+ include Quickery::ActiveRecordExtensions
16
+ end
@@ -0,0 +1,175 @@
1
+ module Quickery
2
+ module ActiveRecordExtensions
3
+ module Callbacks
4
+ class << self
5
+ def included(base)
6
+ base.extend ClassMethods
7
+ base.include InstanceMethods
8
+ base.class_eval do
9
+ before_create :quickery_before_create_callback
10
+ before_update :quickery_before_update_callback
11
+ before_destroy :quickery_before_destroy_callback
12
+ end
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ end
18
+
19
+ module InstanceMethods
20
+ private
21
+
22
+ def quickery_before_create_callback
23
+ model = self.class
24
+ changed_attributes = changes.keys
25
+
26
+ if model.quickery_association_chain_dependers.present?
27
+ model.quickery_association_chain_dependers.each do |association_chain_depender|
28
+ quickery_builder = association_chain_depender.quickery_builder
29
+ depender_column_name = quickery_builder.depender_column_name
30
+ dependee_column_name = quickery_builder.dependee_column_name
31
+
32
+ if changed_attributes.include? association_chain_depender.belongs_to.foreign_key
33
+ if send(association_chain_depender.belongs_to.foreign_key).nil?
34
+ new_value = nil
35
+ else
36
+ dependee_record = association_chain_depender.dependee_record(self)
37
+ new_value = dependee_record.send(dependee_column_name)
38
+ end
39
+
40
+ assign_attributes(depender_column_name => new_value)
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ def quickery_before_update_callback
47
+ model = self.class
48
+ changed_attributes = changes.keys
49
+
50
+ if model.quickery_association_chain_dependers.present?
51
+ model.quickery_association_chain_dependers.each do |association_chain_depender|
52
+ quickery_builder = association_chain_depender.quickery_builder
53
+ depender_column_name = quickery_builder.depender_column_name
54
+ dependee_column_name = quickery_builder.dependee_column_name
55
+
56
+ if changed_attributes.include? association_chain_depender.belongs_to.foreign_key
57
+ if send(association_chain_depender.belongs_to.foreign_key).nil?
58
+ new_value = nil
59
+ else
60
+ dependee_record = association_chain_depender.dependee_record(self)
61
+ new_value = dependee_record.send(dependee_column_name)
62
+ end
63
+
64
+ assign_attributes(depender_column_name => new_value)
65
+ end
66
+ end
67
+ end
68
+
69
+ dependent_records_attributes_to_be_updated = {}
70
+
71
+ if model.quickery_association_chain_dependees.present?
72
+ model.quickery_association_chain_dependees.each do |association_chain_dependee|
73
+ quickery_builder = association_chain_dependee.quickery_builder
74
+ depender_column_name = quickery_builder.depender_column_name
75
+ dependee_column_name = quickery_builder.dependee_column_name
76
+
77
+ if changed_attributes.include? dependee_column_name
78
+ new_value = send(dependee_column_name)
79
+
80
+ dependent_records = association_chain_dependee.dependent_records(self)
81
+ # use the SQL as the uniqueness identifier, so that multiple quickery-attributes dependent-records are updated in one go, instead of updating each
82
+ dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym] ||= {}
83
+ dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym][:dependent_records] ||= dependent_records
84
+ dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym][:new_values] ||= {}
85
+ dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym][:new_values][depender_column_name.to_sym] = new_value
86
+ end
87
+ end
88
+ end
89
+
90
+ if model.quickery_association_chain_intermediaries.present?
91
+ model.quickery_association_chain_intermediaries.each do |association_chain_intermediary|
92
+ quickery_builder = association_chain_intermediary.quickery_builder
93
+ depender_column_name = quickery_builder.depender_column_name
94
+ dependee_column_name = quickery_builder.dependee_column_name
95
+
96
+ if changed_attributes.include? association_chain_intermediary.belongs_to.foreign_key
97
+ if send(association_chain_intermediary.belongs_to.foreign_key).nil?
98
+ new_value = nil
99
+ else
100
+ dependee_record = association_chain_intermediary.dependee_record(self)
101
+ new_value = dependee_record.send(dependee_column_name)
102
+ end
103
+
104
+ dependent_records = association_chain_intermediary.dependent_records(self)
105
+ # use the SQL as the uniqueness identifier, so that multiple quickery-attributes dependent-records are updated in one go, instead of updating each
106
+ dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym] ||= {}
107
+ dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym][:dependent_records] ||= dependent_records
108
+ dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym][:new_values] ||= {}
109
+ dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym][:new_values][depender_column_name.to_sym] = new_value
110
+ end
111
+ end
112
+ end
113
+
114
+ dependent_records_attributes_to_be_updated.each do |sql_identifier, hash|
115
+ dependent_records = hash.fetch(:dependent_records)
116
+ new_values = hash.fetch(:new_values)
117
+
118
+ dependent_records.update_all(new_values)
119
+ end
120
+ end
121
+
122
+ def quickery_before_destroy_callback
123
+ model = self.class
124
+
125
+ dependent_records_attributes_to_be_updated = {}
126
+
127
+ if model.quickery_association_chain_dependees.present?
128
+ model.quickery_association_chain_dependees.each do |association_chain_dependee|
129
+ quickery_builder = association_chain_dependee.quickery_builder
130
+ depender_column_name = quickery_builder.depender_column_name
131
+ dependee_column_name = quickery_builder.dependee_column_name
132
+
133
+ if attributes.keys.include? dependee_column_name
134
+ new_value = nil
135
+
136
+ dependent_records = association_chain_dependee.dependent_records(self)
137
+ # use the SQL as the uniqueness identifier, so that multiple quickery-attributes dependent-records are updated in one go, instead of updating each
138
+ dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym] ||= {}
139
+ dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym][:dependent_records] ||= dependent_records
140
+ dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym][:new_values] ||= {}
141
+ dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym][:new_values][depender_column_name.to_sym] = new_value
142
+ end
143
+ end
144
+ end
145
+
146
+ if model.quickery_association_chain_intermediaries.present?
147
+ model.quickery_association_chain_intermediaries.each do |association_chain_intermediary|
148
+ quickery_builder = association_chain_intermediary.quickery_builder
149
+ depender_column_name = quickery_builder.depender_column_name
150
+ dependee_column_name = quickery_builder.dependee_column_name
151
+
152
+ if attributes.keys.include? association_chain_intermediary.belongs_to.foreign_key
153
+ new_value = nil
154
+
155
+ dependent_records = association_chain_intermediary.dependent_records(self)
156
+ # use the SQL as the uniqueness identifier, so that multiple quickery-attributes dependent-records are updated in one go, instead of updating each
157
+ dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym] ||= {}
158
+ dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym][:dependent_records] ||= dependent_records
159
+ dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym][:new_values] ||= {}
160
+ dependent_records_attributes_to_be_updated[dependent_records.to_sql.to_sym][:new_values][depender_column_name.to_sym] = new_value
161
+ end
162
+ end
163
+ end
164
+
165
+ dependent_records_attributes_to_be_updated.each do |sql_identifier, hash|
166
+ dependent_records = hash.fetch(:dependent_records)
167
+ new_values = hash.fetch(:new_values)
168
+
169
+ dependent_records.update_all(new_values)
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -1,17 +1,21 @@
1
- require 'active_support'
2
-
3
1
  module Quickery
4
2
  module ActiveRecordExtensions
5
3
  module DSL
6
- def self.included(base)
7
- base.extend ClassMethods
8
- base.include InstanceMethods
4
+ class << self
5
+ def included(base)
6
+ base.extend ClassMethods
7
+ base.include InstanceMethods
8
+ end
9
9
  end
10
10
 
11
11
  module ClassMethods
12
- def quickery(&block)
13
- association_builder = AssociationBuilder.new(model: self)
14
- association_builder.instance_exec(&block)
12
+ attr_accessor :quickery_association_chain_dependers
13
+ attr_accessor :quickery_association_chain_dependees
14
+ attr_accessor :quickery_association_chain_intermediaries
15
+
16
+ def quickery(mappings)
17
+ mappings_builder = MappingsBuilder.new(model: self, mappings: mappings.with_indifferent_access)
18
+ mappings_builder.map_attributes
15
19
  end
16
20
  end
17
21
 
@@ -21,8 +25,6 @@ module Quickery
21
25
  new_value = determine_quickery_value(depender_column_name)
22
26
  update_columns(depender_column_name => new_value)
23
27
  end
24
-
25
- true
26
28
  end
27
29
 
28
30
  def determine_quickery_value(depender_column_name)
@@ -30,14 +32,10 @@ module Quickery
30
32
 
31
33
  raise ArgumentError, "No defined quickery builder for #{depender_column_name}. Defined values are #{self.class.quickery_builders.keys}" unless quickery_builder
32
34
 
33
- dependee_record = quickery_builder.first_association_builder._quickery_dependee_record(self)
35
+ dependee_record = quickery_builder.association_chains.first.dependee_record(self)
34
36
  dependee_record.send(quickery_builder.dependee_column_name)
35
37
  end
36
38
  end
37
39
  end
38
40
  end
39
41
  end
40
-
41
- ActiveSupport.on_load(:active_record) do
42
- include Quickery::ActiveRecordExtensions::DSL
43
- end
@@ -0,0 +1,106 @@
1
+ module Quickery
2
+ class AssociationChain
3
+ attr_accessor :quickery_builder
4
+ attr_reader :model
5
+ attr_reader :parent_association_chain
6
+ attr_reader :child_association_chain
7
+ attr_reader :name
8
+ attr_reader :dependee_column_name
9
+ attr_reader :belongs_to
10
+
11
+ def initialize(model:, parent_association_chain: nil, name: nil)
12
+ @model = model
13
+ @parent_association_chain = parent_association_chain
14
+ @name = name
15
+ end
16
+
17
+ def build_children_association_chains(names_left:)
18
+ current_name = names_left.first
19
+
20
+ reflections = @model.reflections
21
+ column_names = @model.column_names
22
+ belongs_to_association_names = reflections.map{ |key, value| value.macro == :belongs_to ? key : nil }.compact
23
+
24
+ if belongs_to_association_names.include? current_name
25
+ @belongs_to = reflections[current_name]
26
+ @child_association_chain = AssociationChain.new(
27
+ model: belongs_to.class_name.constantize,
28
+ parent_association_chain: self,
29
+ name: current_name,
30
+ )
31
+ @child_association_chain.build_children_association_chains(names_left: names_left[1..-1])
32
+ return self
33
+
34
+ elsif column_names.include? current_name
35
+ @dependee_column_name = current_name
36
+ return self
37
+
38
+ else
39
+ raise Quickery::Errors::InvalidAssociationOrAttributeError.new(current_name)
40
+ end
41
+ end
42
+
43
+ def child_association_chains(include_self: false, association_chains: [])
44
+ association_chains << self if include_self
45
+
46
+ if @child_association_chain.nil?
47
+ association_chains
48
+ else
49
+ association_chains << @child_association_chain
50
+ return @child_association_chain.child_association_chains(association_chains: association_chains)
51
+ end
52
+ end
53
+
54
+ def parent_association_chains(include_self: false, association_chains: [])
55
+ association_chains << self if include_self
56
+
57
+ if @parent_association_chain.nil?
58
+ association_chains
59
+ else
60
+ association_chains << @parent_association_chain
61
+ @parent_association_chain.parent_association_chains(association_chains: association_chains)
62
+ end
63
+ end
64
+
65
+ def joins_arg(current_joins_arg = nil)
66
+ if @parent_association_chain.nil?
67
+ current_joins_arg
68
+ else
69
+ if current_joins_arg.nil?
70
+ @parent_association_chain.joins_arg(@name.to_sym)
71
+ else
72
+ @parent_association_chain.joins_arg({ @name.to_sym => current_joins_arg })
73
+ end
74
+ end
75
+ end
76
+
77
+ def dependee_record(from_record)
78
+ raise ArgumentError, 'argument should be an instance of @model' unless from_record.is_a? model
79
+
80
+ child_association_chains(include_self: true).inject(from_record) do |from_record, association_chain|
81
+ if association_chain.belongs_to
82
+ from_record.send(association_chain.belongs_to.name)
83
+ else
84
+ from_record
85
+ end
86
+ end
87
+ end
88
+
89
+ def dependent_records(from_record)
90
+ primary_key_value = from_record.send(from_record.class.primary_key)
91
+ most_parent_model = parent_association_chains.last.model
92
+
93
+ records = most_parent_model.all
94
+
95
+ unless (joins_arg_tmp = joins_arg).nil?
96
+ records = records.joins(joins_arg_tmp)
97
+ end
98
+
99
+ records = records.where(
100
+ model.table_name => {
101
+ model.primary_key => primary_key_value
102
+ }
103
+ )
104
+ end
105
+ end
106
+ end
@@ -2,105 +2,38 @@ module Quickery
2
2
  class CallbacksBuilder
3
3
  attr_reader :quickery_builder
4
4
 
5
- def initialize(quickery_builder:, should_add_callbacks: true)
5
+ def initialize(quickery_builder:)
6
6
  @quickery_builder = quickery_builder
7
- add_callbacks if should_add_callbacks
8
7
  end
9
8
 
10
- private
11
-
12
- def add_callbacks
13
- add_callback_to_depender_model
14
- add_callback_to_dependee_model
15
- add_callback_to_each_intermediate_model
9
+ def build_callbacks
10
+ build_callback_to_depender_model
11
+ build_callback_to_dependee_model
12
+ build_callback_to_each_intermediate_model
16
13
  end
17
14
 
18
- # add callback to immediately sync value after a record has been created / updated
19
- def add_callback_to_depender_model
20
- first_association_builder = @quickery_builder.first_association_builder
21
- depender_column_name = @quickery_builder.depender_column_name
22
- dependee_column_name = @quickery_builder.dependee_column_name
23
-
24
- first_association_builder.model.class_exec do
25
-
26
- # before create or update
27
- before_save do
28
- if changes.keys.include? first_association_builder.belongs_to.foreign_key
29
- if send(first_association_builder.belongs_to.foreign_key).nil?
30
- new_value = nil
31
- else
32
- dependee_record = first_association_builder._quickery_dependee_record(self)
33
- new_value = dependee_record.send(dependee_column_name)
34
- end
15
+ private
35
16
 
36
- assign_attributes(depender_column_name => new_value)
37
- end
38
- end
39
- end
17
+ # add callback to immediately sync value after a record has been created / updated
18
+ def build_callback_to_depender_model
19
+ first_association_chain = @quickery_builder.association_chains.first
20
+ first_association_chain.model.quickery_association_chain_dependers ||= []
21
+ first_association_chain.model.quickery_association_chain_dependers << first_association_chain
40
22
  end
41
23
 
42
24
  # add callback to sync changes when dependee_column has been updated
43
- def add_callback_to_dependee_model
44
- last_association_builder = @quickery_builder.last_association_builder
45
- depender_column_name = @quickery_builder.depender_column_name
46
- dependee_column_name = @quickery_builder.dependee_column_name
47
-
48
- last_association_builder.model.class_exec do
49
-
50
- before_update do
51
- if changes.keys.include? dependee_column_name
52
- new_value = send(dependee_column_name)
53
-
54
- dependent_records = last_association_builder._quickery_dependent_records(self)
55
- dependent_records.update_all(depender_column_name => new_value)
56
- end
57
- end
58
-
59
- before_destroy do
60
- if attributes.keys.include? dependee_column_name
61
- new_value = nil
62
-
63
- dependent_records = last_association_builder._quickery_dependent_records(self)
64
- dependent_records.update_all(depender_column_name => new_value)
65
- end
66
- end
67
- end
25
+ def build_callback_to_dependee_model
26
+ last_association_chain = @quickery_builder.association_chains.last
27
+ last_association_chain.model.quickery_association_chain_dependees ||= []
28
+ last_association_chain.model.quickery_association_chain_dependees << last_association_chain
68
29
  end
69
30
 
70
31
  # also add callbacks to sync changes when intermediary associations have been changed (this does not include first and last builder)
71
- def add_callback_to_each_intermediate_model
72
- last_association_builder = @quickery_builder.last_association_builder
73
- depender_column_name = @quickery_builder.depender_column_name
74
- dependee_column_name = @quickery_builder.dependee_column_name
75
-
76
- last_association_builder._quickery_get_parent_builders(include_self: true)[1..-2].each do |association_builder|
77
- intermediate_model = association_builder.model
78
-
79
- intermediate_model.class_exec do
80
-
81
- before_update do
82
- if changes.keys.include? association_builder.belongs_to.foreign_key
83
- if send(association_builder.belongs_to.foreign_key).nil?
84
- new_value = nil
85
- else
86
- dependee_record = association_builder._quickery_dependee_record(self)
87
- new_value = dependee_record.send(dependee_column_name)
88
- end
89
-
90
- dependent_records = association_builder._quickery_dependent_records(self)
91
- dependent_records.update_all(depender_column_name => new_value)
92
- end
93
- end
94
-
95
- before_destroy do
96
- if attributes.keys.include? association_builder.belongs_to.foreign_key
97
- new_value = nil
98
-
99
- dependent_records = last_association_builder._quickery_dependent_records(self)
100
- dependent_records.update_all(depender_column_name => new_value)
101
- end
102
- end
103
- end
32
+ def build_callback_to_each_intermediate_model
33
+ last_association_chain = @quickery_builder.association_chains.last
34
+ last_association_chain.parent_association_chains(include_self: true)[1..-2].each do |association_chain|
35
+ association_chain.model.quickery_association_chain_intermediaries ||= []
36
+ association_chain.model.quickery_association_chain_intermediaries << association_chain
104
37
  end
105
38
  end
106
39
  end
@@ -0,0 +1,10 @@
1
+ module Quickery
2
+ module Errors
3
+ class InvalidAssociationOrAttributeError < StandardError
4
+ def initialize(name)
5
+ message = "#{name} is not a valid association or attribute name"
6
+ super(message)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,38 @@
1
+ module Quickery
2
+ class MappingsBuilder
3
+ attr_reader :model
4
+ attr_reader :mappings
5
+
6
+ def initialize(model:, mappings:)
7
+ @model = model
8
+ @mappings = mappings
9
+ end
10
+
11
+ # https://stackoverflow.com/questions/9647997/converting-a-nested-hash-into-a-flat-hash
12
+ def flat_hash(hash, k = [])
13
+ return {k => hash} unless hash.is_a?(Hash)
14
+ hash.inject({}){ |h, v| h.merge! flat_hash(v[-1], k + [v[0]]) }
15
+ end
16
+
17
+ def map_attributes
18
+ flat_hash(@mappings).each do |names, depender_column_name|
19
+ first_association_chain = AssociationChain.new(model: model)
20
+ first_association_chain.build_children_association_chains(names_left: names)
21
+
22
+ all_association_chains = first_association_chain.child_association_chains(include_self: true)
23
+ dependee_column_name = all_association_chains.last.dependee_column_name
24
+
25
+ quickery_builder = QuickeryBuilder.new(
26
+ model: @model,
27
+ association_chains: all_association_chains,
28
+ dependee_column_name: dependee_column_name,
29
+ depender_column_name: depender_column_name,
30
+ )
31
+
32
+ quickery_builder.add_to_model
33
+ quickery_builder.add_to_association_chains
34
+ quickery_builder.create_model_callbacks
35
+ end
36
+ end
37
+ end
38
+ end
@@ -3,28 +3,32 @@ module Quickery
3
3
  attr_reader :model
4
4
  attr_reader :depender_column_name
5
5
  attr_reader :dependee_column_name
6
- attr_reader :first_association_builder
7
- attr_reader :last_association_builder
8
- attr_reader :callbacks_builder
6
+ attr_reader :association_chains
9
7
 
10
- def initialize(dependee_column_name:, last_association_builder:)
8
+ def initialize(model:, association_chains:, dependee_column_name:, depender_column_name:)
9
+ @model = model
10
+ @association_chains = association_chains
11
11
  @dependee_column_name = dependee_column_name
12
- @last_association_builder = last_association_builder
13
- @first_association_builder = last_association_builder._quickery_get_parent_builders.last
14
- @model = @first_association_builder.model
15
- end
16
-
17
- def ==(depender_column_name)
18
12
  @depender_column_name = depender_column_name
13
+ end
19
14
 
20
- @callbacks_builder = CallbacksBuilder.new(quickery_builder: self)
21
-
15
+ def add_to_model
22
16
  define_quickery_builders_in_model_class unless @model.respond_to? :quickery_builders
23
-
24
17
  # include this to the list of quickery builders defined for this model
25
18
  @model.quickery_builders[depender_column_name] = self
26
19
  end
27
20
 
21
+ def add_to_association_chains
22
+ association_chains.each do |association_chain|
23
+ association_chain.quickery_builder = self
24
+ end
25
+ end
26
+
27
+ def create_model_callbacks
28
+ @callbacks_builder = CallbacksBuilder.new(quickery_builder: self)
29
+ @callbacks_builder.build_callbacks
30
+ end
31
+
28
32
  private
29
33
 
30
34
  def define_quickery_builders_in_model_class
@@ -1,3 +1,3 @@
1
1
  module Quickery
2
- VERSION = '0.1.4'
2
+ VERSION = '1.0.0'
3
3
  end
@@ -7,6 +7,7 @@
7
7
  * supports `has_many` and `belongs_to` while Quickery only supports `belongs_to` (currently)
8
8
  * Cons against Quickery:
9
9
  * It is not documented, but you'll need to do `Rails.application.eager_load!` first or something similar, to first load all the models as otherwise defined `persistize` across the models will be autoloaded, and therefore will not work properly. Quickery already does this eager loading of models out of the box. For more info, see [lib/quickery/railtie.rb](lib/quickery/railtie.rb)
10
+ * batch-update in one go for multiple quickery-defined attributes instead of updating each attribute/method
10
11
  * loops through each children records and update each attribute which can get very slow with lots of children associated records, and therefore as many number of SQL UPDATE queries vs Quickery which uses `update_all` which is just one update query, and does not loop through children records:
11
12
 
12
13
  ```ruby
@@ -113,6 +114,7 @@
113
114
  * allows "updates" as a background process
114
115
  * Cons against Quickery:
115
116
  * Rails 5 is not part of its supported list in their github page. And just to try it out on a Rails 5 app, `Flattery::ValueProvider` did not seem to work, because values are not pushed to the `:notes`'s `:category_name` values.
117
+ * batch-update in one go for multiple quickery-defined attributes instead of updating each
116
118
  * Using Rails 4, does not support nested associated dependencies for `belongs_to`:
117
119
 
118
120
  ```ruby
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quickery
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jules Roman Polidario
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-09-13 00:00:00.000000000 Z
11
+ date: 2018-10-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -248,9 +248,13 @@ files:
248
248
  - gemfiles/rails_4.gemfile
249
249
  - gemfiles/rails_5.gemfile
250
250
  - lib/quickery.rb
251
+ - lib/quickery/active_record_extensions.rb
252
+ - lib/quickery/active_record_extensions/callbacks.rb
251
253
  - lib/quickery/active_record_extensions/dsl.rb
252
- - lib/quickery/association_builder.rb
254
+ - lib/quickery/association_chain.rb
253
255
  - lib/quickery/callbacks_builder.rb
256
+ - lib/quickery/errors/invalid_association_or_attribute_error.rb
257
+ - lib/quickery/mappings_builder.rb
254
258
  - lib/quickery/quickery_builder.rb
255
259
  - lib/quickery/railtie.rb
256
260
  - lib/quickery/version.rb
@@ -1,108 +0,0 @@
1
- module Quickery
2
- class AssociationBuilder
3
- attr_reader :model
4
- attr_reader :parent_builder
5
- attr_reader :child_builder
6
- attr_reader :inverse_association_name
7
- attr_reader :belongs_to
8
-
9
- def initialize(model:, parent_builder: nil, inverse_association_name: nil)
10
- @model = model
11
- @parent_builder = parent_builder
12
- @inverse_association_name = inverse_association_name
13
- @reflections = model.reflections
14
- @belongs_to_association_names = @reflections.map{ |key, value| value.macro == :belongs_to ? key : nil }.compact
15
- @column_names = model.column_names
16
- end
17
-
18
- # we need to prepend _quickery to all methods, to make sure no conflicts with association names that are dynamically invoked through `method_missing` below
19
-
20
- def _quickery_get_child_builders(include_self: false, builders: [])
21
- builders << self if include_self
22
-
23
- if @child_builder.nil?
24
- builders
25
- else
26
- builders << @child_builder
27
- return @child_builder._quickery_get_child_builders(builders: builders)
28
- end
29
- end
30
-
31
- def _quickery_get_parent_builders(include_self: false, builders: [])
32
- builders << self if include_self
33
-
34
- if @parent_builder.nil?
35
- builders
36
- else
37
- builders << @parent_builder
38
- @parent_builder._quickery_get_parent_builders(builders: builders)
39
- end
40
- end
41
-
42
- def _quickery_get_joins_arg(current_joins_arg = nil)
43
- if @parent_builder.nil?
44
- current_joins_arg
45
- else
46
- if current_joins_arg.nil?
47
- @parent_builder._quickery_get_joins_arg(@inverse_association_name.to_sym)
48
- else
49
- @parent_builder._quickery_get_joins_arg({ @inverse_association_name.to_sym => current_joins_arg })
50
- end
51
- end
52
- end
53
-
54
- def _quickery_dependent_records(record_to_be_saved)
55
- primary_key_value = record_to_be_saved.send(record_to_be_saved.class.primary_key)
56
- most_parent_model = _quickery_get_parent_builders.last.model
57
-
58
- records = most_parent_model.all
59
-
60
- unless (joins_arg = _quickery_get_joins_arg).nil?
61
- records = records.joins(joins_arg)
62
- end
63
-
64
- records = records.where(
65
- model.table_name => {
66
- model.primary_key => primary_key_value
67
- }
68
- )
69
- end
70
-
71
- def _quickery_dependee_record(record_to_be_saved)
72
- raise ArgumentError, 'argument should be an instance of @model' unless record_to_be_saved.is_a? model
73
-
74
- _quickery_get_child_builders(include_self: true).inject(record_to_be_saved) do |record, association_builder|
75
- if association_builder.belongs_to
76
- record.send(association_builder.belongs_to.name)
77
- else
78
- record
79
- end
80
- end
81
- end
82
-
83
- private
84
-
85
- def method_missing(method_name, *args, &block)
86
- method_name_str = method_name.to_s
87
- if @belongs_to_association_names.include? method_name_str
88
- @belongs_to = @reflections[method_name_str]
89
- @child_builder = AssociationBuilder.new(model: belongs_to.class_name.constantize, parent_builder: self, inverse_association_name: method_name_str)
90
- elsif @column_names.include? method_name_str
91
- QuickeryBuilder.new(dependee_column_name: method_name_str, last_association_builder: self)
92
- else
93
- super
94
- end
95
- end
96
-
97
- def respond_to_missing(method_name, include_private = false)
98
- method_name_str = method_name.to_s
99
- if @belongs_to_association_names.include? method_name_str
100
- true
101
- elsif @column_names.include? method_name_str
102
- true
103
- else
104
- super
105
- end
106
- end
107
- end
108
- end