volt-repo_cache 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
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