volt-repo_cache 0.1.4

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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +12 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +314 -0
  7. data/Rakefile +1 -0
  8. data/lib/volt/repo_cache.rb +6 -0
  9. data/lib/volt/repo_cache/association.rb +100 -0
  10. data/lib/volt/repo_cache/cache.rb +116 -0
  11. data/lib/volt/repo_cache/collection.rb +259 -0
  12. data/lib/volt/repo_cache/model.rb +671 -0
  13. data/lib/volt/repo_cache/model_array.rb +169 -0
  14. data/lib/volt/repo_cache/util.rb +78 -0
  15. data/lib/volt/repo_cache/version.rb +5 -0
  16. data/spec/dummy/.gitignore +9 -0
  17. data/spec/dummy/README.md +4 -0
  18. data/spec/dummy/app/main/assets/css/app.css.scss +1 -0
  19. data/spec/dummy/app/main/config/dependencies.rb +11 -0
  20. data/spec/dummy/app/main/config/initializers/boot.rb +10 -0
  21. data/spec/dummy/app/main/config/routes.rb +14 -0
  22. data/spec/dummy/app/main/controllers/main_controller.rb +27 -0
  23. data/spec/dummy/app/main/models/customer.rb +4 -0
  24. data/spec/dummy/app/main/models/order.rb +6 -0
  25. data/spec/dummy/app/main/models/product.rb +5 -0
  26. data/spec/dummy/app/main/models/user.rb +12 -0
  27. data/spec/dummy/app/main/views/main/about.html +7 -0
  28. data/spec/dummy/app/main/views/main/index.html +6 -0
  29. data/spec/dummy/app/main/views/main/main.html +29 -0
  30. data/spec/dummy/config.ru +4 -0
  31. data/spec/dummy/config/app.rb +147 -0
  32. data/spec/dummy/config/base/index.html +15 -0
  33. data/spec/dummy/config/initializers/boot.rb +4 -0
  34. data/spec/integration/sample_integration_spec.rb +11 -0
  35. data/spec/sample_spec.rb +7 -0
  36. data/spec/spec_helper.rb +18 -0
  37. data/volt-repo_cache.gemspec +38 -0
  38. metadata +287 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9cb06385cea5ffc9a2c6518ec011d99c65957550
4
+ data.tar.gz: b88fab46ec514ac59bf7da97a84e2c3461fee23b
5
+ SHA512:
6
+ metadata.gz: ff6af4ea44e4067961a8a3fd289004736538ab165cec33c8a258ae7c5cb770cc120f9d4267adb179a1843ccd6b26df349d0b4683a51bfb324bb22f3a3199b641
7
+ data.tar.gz: e0616aded5eaff2f4079a7a28e645baf48004e12d5eaeb8119c7086b5266f7cd3ac32d76b4c0d1ea053e0889215be45ed8e73359ad35fb56922aad12aa9ae6b3
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ make
2
+ .idea
3
+ *.gem
4
+ *.rbc
5
+ .bundle
6
+ .config
7
+ .yardoc
8
+ Gemfile.lock
9
+ InstalledFiles
10
+ _yardoc
11
+ coverage
12
+ doc/
13
+ lib/bundler/man
14
+ pkg
15
+ rdoc
16
+ spec/reports
17
+ test/tmp
18
+ test/version_tmp
19
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in volt-repo_cache.gemspec
4
+ gemspec
5
+
6
+ # Optional Gems for testing/dev
7
+
8
+ # The implementation of ReadWriteLock in Volt uses concurrent ruby and ext helps performance.
9
+ gem 'concurrent-ruby-ext', '~> 0.8.0'
10
+
11
+ # Gems you use for development should be added to the gemspec file as
12
+ # development dependencies.
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Colin Gunn
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,314 @@
1
+ # Volt::RepoCache
2
+
3
+ - Provides client-side caching of repository (db) collections, models and their associations.
4
+ - Loads multiple associated collections (or query based subsets) into a cache.
5
+ - Buffers changes to models, collections and associations until flushed.
6
+ - Allows for flushes to be performed at model, collection or cache level.
7
+ - Provides increased associational integrity.
8
+ - Reduces the burden of promise handling in repository (db) operations.
9
+ - Is ideal for use where multiple associated models are being displayed and edited.
10
+ - Preserves standard Volt model and collection interfaces and reactivity.
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ gem 'volt-repo_cache'
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ Or install it yourself as:
23
+
24
+ $ gem install volt-repo_cache
25
+
26
+ ## Usage
27
+
28
+ Assume we have a sales application with three model classes:
29
+
30
+ class Customer < Volt::Model
31
+ field :name
32
+ has_many :orders
33
+ end
34
+
35
+ class Product < Volt::Model
36
+ field :name
37
+ field :price
38
+ has_many :orders
39
+ end
40
+
41
+ class Order < Volt:Model
42
+ belongs_to :customer
43
+ belongs_to :product
44
+ field :date
45
+ field :quantity
46
+ end
47
+
48
+ Let's say we want to cache all customers, products,
49
+ and orders, the latter between some given dates.
50
+
51
+ The following code will create the cache and load it
52
+ in a controller's `index` method. We'll also add a
53
+ `before_index_remove` method to clear the cache
54
+ when leaving the page.
55
+
56
+ #### Example 1 - defining and loading a cache
57
+
58
+ class OrderController < Volt::ModelController
59
+
60
+ def index
61
+ new_cache.loaded.then do |cache|
62
+ page._cache = cache
63
+ end.fail do |errors|
64
+ flashes << errors.to_s
65
+ end
66
+ end
67
+
68
+ def new_cache
69
+ Volt::RepoCache.new(
70
+ Volt.current_app.store,
71
+ customer: {
72
+ has_many: :orders,
73
+ }
74
+ product: {
75
+ has_many: :orders,
76
+ }
77
+ order: {
78
+ belongs_to: [:customer, :product]
79
+ where: {'$and' => [:date => {'$gte' => start_date}, :date => {'$lte' => end_date}]}
80
+ }
81
+ )
82
+ end
83
+
84
+ def before_index_remove
85
+ page._cache.clear if _cache
86
+ end
87
+
88
+ ...
89
+ end
90
+
91
+ **Under Volt 0.9.7 the specification of associations
92
+ will be provided by the underlying models' class
93
+ definitions and will no longer be required in
94
+ the cache options.**
95
+
96
+ In the `index` method we only need to resolve
97
+ one promise when the cache is `loaded`.
98
+
99
+ Collections may be identified in the singular or plural
100
+ according to preference, e.g. `order:` or `orders:`,
101
+ with or without an underscore prefix.
102
+
103
+ A `where:` or `query:` option may be provided for each collection
104
+ to specify which models are loaded from the repository.
105
+ The default behaviour is to load all models in a collection.
106
+
107
+ Resolution of associations between cached models will
108
+ depend on what has been loaded into the cache for
109
+ each collection.
110
+
111
+ After the cache is loaded you can then access
112
+ collections, models and associations without
113
+ handling promise resolution or failure.
114
+
115
+ Otherwise, the interfaces to cached models and collections
116
+ largely behave as normal.
117
+
118
+ #### Example 2 - query and association resolution
119
+
120
+ # find all orders for customer 'ABC and product 'XYZ'
121
+ cache._customers.where(name: 'ABC').orders.select { |order|
122
+ order.product.name == 'XYZ'
123
+ }
124
+
125
+ Unlike a standard Volt query and association call
126
+ (`order.product`) we have no intervening promise(s) to
127
+ resolve, and also avoid relatively slow database request(s).
128
+
129
+ #### Example 3 - query and association resolution
130
+
131
+ # total cost of products ordered by customer
132
+ customer = cache._customers.where(name: 'ABC')
133
+ total_cost = customer.orders.reduce(0) do |sum, order|
134
+ sum + (order.quantity * order.product.price)
135
+ end
136
+
137
+ Again, no promises to resolve and faster calculation of
138
+ total cost than would be the case with uncached database
139
+ access.
140
+
141
+ ### Changes and flushing
142
+
143
+ Changes to field values in models are buffered until
144
+ flushed (saved) to the database. Flushes may be requested
145
+ at the model, collection or cache level. Each flush
146
+ returns a single promise. Some examples:
147
+
148
+ #### Example 4 - change and save a single model
149
+
150
+ # change the price of a product and save it
151
+ product = cache._products.where(name: 'XYZ')
152
+ product.price = 9.99
153
+ # flush the product model
154
+ product.flush!.then do |result|
155
+ puts "#{result} saved"
156
+ end.fail do |errors|
157
+ puts errors
158
+ end
159
+
160
+ #### Example 5 - change and save several models in a collection
161
+
162
+ # change the price of multiple products
163
+ # and save them all together
164
+ products = cache._products
165
+ products.where(name: 'X').price = 7.77
166
+ products.where(name: 'Y').price = 8.88
167
+ products.where(name: 'Z').price = 9.99
168
+ # flush the 'products' collection
169
+ products.flush!.then do |result|
170
+ puts "all products saved"
171
+ end.fail do |errors|
172
+ puts "error saving products: #{errors}"
173
+ end
174
+
175
+ #### Example 6 - change and save models in more than one collection
176
+
177
+ # change the price of a product
178
+ # and the name of a customer
179
+ # and save them together
180
+ cache._products.where(name: 'XYZ').price = 7.77
181
+ cache._customer.where(name: 'ABC').name = 'EFG'
182
+ # flush the whole cache
183
+ cache.flush!.then do |result|
184
+ puts "cached flushed successfully"
185
+ end.fail do |errors|
186
+ puts "error flushing cache: #{errors}"
187
+ end
188
+
189
+ ### Creating new models with no owners
190
+
191
+ There are two ways to create a new instance of a model
192
+ not belonging to another model:
193
+
194
+ #### Example 7 - create a new model (with no owner) via a collection
195
+
196
+ # create a new product
197
+ p = Product.new(name: 'IJK')
198
+ cache._products << p
199
+
200
+ A new model must be added to the appropriate cached collection
201
+ (using `#<<` or `#append`) before it also is cached. It will
202
+ not be saved to the database until the model or its containing
203
+ collection or cache is flushed.
204
+
205
+ NB Both `#<<` and `#append` return the collection, not the
206
+ appended model.
207
+
208
+ Another way of creating a new model via a collection using a hash:
209
+
210
+ #### Example 8 - create a new model (with no owner) via a collection
211
+
212
+ # create a new product
213
+ cache._products << {name: 'IJK'}
214
+ p = cache._products.where(name: 'IJK')
215
+
216
+ ### Creating new models with owners
217
+
218
+ When creating a new model which belongs to one or more models
219
+ you must set the foreign key id(s) to establish the association(s).
220
+
221
+ #### Example 9 - create a new model (with two owners) via a collection
222
+
223
+ # create a new order which belongs to a customer and a product
224
+ product = cache._products.where(code: 'XYZ')
225
+ customer = cache._customers.where(code: 'ABC')
226
+ order = Order.new(product_id: product.id, customer_id: customer.id, quantity: 1, date: Date.today)
227
+ cache._orders << order
228
+
229
+ An easier way is ask an owner model to create a new owned model:
230
+
231
+ #### Example 10 - create a new model (with two owners) via an owner model
232
+
233
+ product = cache._products.where(code: 'XYZ')
234
+ customer = cache._customers.where(code: 'ABC')
235
+ # ask the customer to create a new order, give it the product id
236
+ order = customer.new_order(product_id: product.id, quantity: 1, date: Date.today)
237
+
238
+ ### Destroying models
239
+
240
+ Models in the cache can be marked for destruction when the cache is flushed using `#mark_for_destruction!`.
241
+ Still to do - associational integrity checks when marking for destruction.
242
+
243
+ ## Warnings
244
+
245
+ **Flushes to the underlying repository are not atomic and cannot be rolled back**.
246
+ If part of the cache/collection/model/association
247
+ flush fails the transaction(s) may lose integrity.
248
+
249
+ The cached models and collections contain circular references
250
+ (the models refer to the collection which contains them and
251
+ collections refer to the cache). Not being sure what the
252
+ implications are for efficient garbage collection (in Ruby
253
+ on the server and Javascript on the client), a method
254
+ is provided to clear the cache when it is no longer required,
255
+ breaking all internal (circular) references.
256
+
257
+ ## TODO
258
+
259
+ 1. Use associations_data in Volt::Models when 0.9.7 (sql) version available.
260
+ 2. Handle non-standard collection, foreign_key and local_key Volt model options.
261
+ 3. Association integrity checks on mark_for_destruction!
262
+ 4. Test spec.
263
+ 5. Locking?
264
+ 6. Atomic transactions?
265
+ 7. Removal of circular references?
266
+
267
+ ## Contributing and use
268
+
269
+ This gem was written as part of the development of a production
270
+ application, primarily to speed up processing requiring many
271
+ implicit database queries (across associated collections), as well
272
+ as simplifying association management and reducing the burden of
273
+ asynchronous promise resolution.
274
+
275
+ It works well enough for our current application's needs,
276
+ but it may not be suitable for all requirements.
277
+
278
+ We will look at extending the cache framework to support locking and
279
+ atomic transactions (with rollback), but in the meantime if you have
280
+ a need or interest in this area your suggestions and contributions
281
+ are very welcome.
282
+
283
+ To contribute:
284
+
285
+ 1. Fork it ( http://github.com/[my-github-username]/volt-repo_cache/fork )
286
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
287
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
288
+ 4. Push to the branch (`git push origin my-new-feature`)
289
+ 5. Create new Pull Request
290
+
291
+ ## License
292
+
293
+ Copyright (c) 2015 Colin Gunn
294
+
295
+ MIT License
296
+
297
+ Permission is hereby granted, free of charge, to any person obtaining
298
+ a copy of this software and associated documentation files (the
299
+ "Software"), to deal in the Software without restriction, including
300
+ without limitation the rights to use, copy, modify, merge, publish,
301
+ distribute, sublicense, and/or sell copies of the Software, and to
302
+ permit persons to whom the Software is furnished to do so, subject to
303
+ the following conditions:
304
+
305
+ The above copyright notice and this permission notice shall be
306
+ included in all copies or substantial portions of the Software.
307
+
308
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
309
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
310
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
311
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
312
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
313
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
314
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,6 @@
1
+ require 'volt/repo_cache/util'
2
+ require 'volt/repo_cache/model_array'
3
+ require 'volt/repo_cache/association'
4
+ require 'volt/repo_cache/model'
5
+ require 'volt/repo_cache/collection'
6
+ require 'volt/repo_cache/cache'
@@ -0,0 +1,100 @@
1
+ module Volt
2
+ module RepoCache
3
+ class Association
4
+ include Volt::RepoCache::Util
5
+
6
+ attr_reader :local_name_singular, :local_name_plural
7
+ attr_reader :local_collection
8
+ attr_reader :foreign_name, :foreign_collection_name
9
+ attr_reader :foreign_model_class_name, :foreign_model_class
10
+ attr_reader :type, :foreign_id_field, :local_id_field
11
+
12
+ def initialize(local_collection, foreign_name, type)
13
+ _local_name = local_collection.name.to_s.sub(/^_/, '')
14
+ @local_name_singular = _local_name.singularize.to_sym
15
+ @local_name_plural = _local_name.pluralize.to_sym
16
+ @local_collection = local_collection
17
+ @foreign_name = foreign_name
18
+ @type = type
19
+ @foreign_model_class_name = @foreign_name.to_s.singularize.camelize
20
+ @foreign_model_class = Object.const_get(@foreign_model_class_name)
21
+ @foreign_collection_name = :"_#{@foreign_name.to_s.pluralize}"
22
+ @foreign_id_field = has_any? ? :"#{@local_collection.model_class_name.underscore}_id" : :id
23
+ @local_id_field = belongs_to? ? :"#{@foreign_name.to_s}_id" : :id
24
+ end
25
+
26
+ # Hide circular references to local
27
+ # and foreign collections for inspection.
28
+ def inspect
29
+ __local = @local_collection
30
+ __foreign = @foreign_collection
31
+ @local_collection = "{{#{@local_collection ? @local_collection.name : :nil}}"
32
+ @foreign_collection = "{{#{@foreign_collection ? @foreign_collection.name : :nil}}"
33
+ result = super
34
+ @local_collection = __local
35
+ @foreign_collection = __foreign
36
+ result
37
+ end
38
+
39
+ def cache
40
+ @local_collection.cache
41
+ end
42
+
43
+ # Must be lazy initialization since we
44
+ # don't know order in which collections
45
+ # will be loaded to cache.
46
+ def foreign_collection
47
+ @foreign_collection ||= cache.collections[@foreign_collection_name]
48
+ end
49
+
50
+ # Returns the reciprocal association
51
+ # which may be nil if the foreign_collection
52
+ # is not interested (has not specified)
53
+ # the reciprocal association.
54
+ # It may be, for example, that this association
55
+ # is a belongs_to, but there is no reciprocal
56
+ # has_one or has_many association in the 'owner'.
57
+ # Must be lazy initialization since it depends on
58
+ # foreign_collection being lazily initialized.
59
+ def reciprocal
60
+ unless @reciprocal
61
+ # debug __method__, __LINE__, ""
62
+ @reciprocal = foreign_collection.associations.values.detect do |a|
63
+ # debug __method__, __LINE__, "#{a.foreign_collection.name} ?==? #{local_collection.name}"
64
+ a.foreign_collection.name == local_collection.name
65
+ end
66
+ @reciprocal = :nil unless @reciprocal
67
+ # debug __method__, __LINE__, "reciprocal of #{self.inspect} is #{@reciprocal.inspect}"
68
+ end
69
+ @reciprocal == :nil ? nil : @reciprocal
70
+ end
71
+
72
+ def reciprocated?
73
+ !!reciprocal
74
+ end
75
+
76
+ def has_one?
77
+ type == :has_one
78
+ end
79
+
80
+ def has_many?
81
+ type == :has_many
82
+ end
83
+
84
+ def has_any?
85
+ has_one? || has_many?
86
+ end
87
+
88
+ def belongs_to?
89
+ type == :belongs_to
90
+ end
91
+
92
+ private
93
+
94
+ def uncache
95
+ @local_collection = @foreign_collection = @reciprocal = nil
96
+ end
97
+
98
+ end
99
+ end
100
+ end