hyper-model 0.6.0 → 0.99.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.
Files changed (140) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +35 -41
  3. data/.rspec +2 -0
  4. data/.travis.yml +33 -0
  5. data/CHANGELOG.md +34 -0
  6. data/DOCS.md +735 -0
  7. data/Gemfile +7 -0
  8. data/Gemfile.lock +298 -224
  9. data/{LICENSE → LICENSE.txt} +6 -6
  10. data/README.md +51 -2
  11. data/Rakefile +18 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +7 -0
  14. data/codeship.database.yml +18 -0
  15. data/hyper-model.gemspec +62 -36
  16. data/lib/active_model_client_stubs.rb +16 -0
  17. data/lib/active_record_base.rb +331 -0
  18. data/{examples/chat-app/app/assets/images/.keep → lib/acts_as_string.rb} +0 -0
  19. data/lib/enumerable/pluck.rb +6 -0
  20. data/lib/hyper-model.rb +59 -8
  21. data/lib/hyper_model/version.rb +3 -0
  22. data/lib/hyper_react/input_tags.rb +47 -0
  23. data/lib/hyperloop/model/load.rb +1 -1
  24. data/lib/kernel/itself.rb +5 -0
  25. data/lib/object/tap.rb +7 -0
  26. data/lib/opal/equality_patches.rb +15 -0
  27. data/lib/opal/parse_patch.rb +14 -0
  28. data/lib/opal/set_patches.rb +8 -0
  29. data/lib/reactive_record/active_record/aggregations.rb +69 -0
  30. data/lib/reactive_record/active_record/associations.rb +118 -0
  31. data/lib/reactive_record/active_record/base.rb +10 -0
  32. data/lib/reactive_record/active_record/class_methods.rb +406 -0
  33. data/lib/reactive_record/active_record/error.rb +31 -0
  34. data/lib/reactive_record/active_record/errors.rb +374 -0
  35. data/lib/reactive_record/active_record/instance_methods.rb +187 -0
  36. data/lib/reactive_record/active_record/public_columns_hash.rb +44 -0
  37. data/lib/reactive_record/active_record/reactive_record/backing_record_inspector.rb +36 -0
  38. data/lib/reactive_record/active_record/reactive_record/base.rb +416 -0
  39. data/lib/reactive_record/active_record/reactive_record/collection.rb +558 -0
  40. data/lib/reactive_record/active_record/reactive_record/column_types.rb +75 -0
  41. data/lib/reactive_record/active_record/reactive_record/dummy_value.rb +236 -0
  42. data/lib/reactive_record/active_record/reactive_record/getters.rb +133 -0
  43. data/lib/reactive_record/active_record/reactive_record/isomorphic_base.rb +576 -0
  44. data/lib/reactive_record/active_record/reactive_record/lookup_tables.rb +54 -0
  45. data/lib/reactive_record/active_record/reactive_record/operations.rb +107 -0
  46. data/lib/reactive_record/active_record/reactive_record/scoped_collection.rb +62 -0
  47. data/lib/reactive_record/active_record/reactive_record/setters.rb +194 -0
  48. data/lib/reactive_record/active_record/reactive_record/unscoped_collection.rb +16 -0
  49. data/lib/reactive_record/active_record/reactive_record/while_loading.rb +343 -0
  50. data/lib/reactive_record/active_record_error.rb +4 -0
  51. data/lib/reactive_record/broadcast.rb +223 -0
  52. data/lib/reactive_record/engine.rb +11 -0
  53. data/lib/reactive_record/interval.rb +190 -0
  54. data/lib/reactive_record/permissions.rb +117 -0
  55. data/lib/reactive_record/pry.rb +13 -0
  56. data/lib/reactive_record/reactive_scope.rb +18 -0
  57. data/lib/reactive_record/scope_description.rb +121 -0
  58. data/lib/reactive_record/serializers.rb +7 -0
  59. data/lib/reactive_record/server_data_cache.rb +478 -0
  60. data/path_release_steps.md +9 -0
  61. metadata +399 -109
  62. data/CODE_OF_CONDUCT.md +0 -49
  63. data/examples/chat-app/.gitignore +0 -21
  64. data/examples/chat-app/Gemfile +0 -62
  65. data/examples/chat-app/Gemfile.lock +0 -309
  66. data/examples/chat-app/README.md +0 -3
  67. data/examples/chat-app/Rakefile +0 -6
  68. data/examples/chat-app/app/assets/config/manifest.js +0 -3
  69. data/examples/chat-app/app/assets/javascripts/application.js +0 -3
  70. data/examples/chat-app/app/assets/stylesheets/application.scss +0 -33
  71. data/examples/chat-app/app/controllers/application_controller.rb +0 -3
  72. data/examples/chat-app/app/controllers/home_controller.rb +0 -5
  73. data/examples/chat-app/app/hyperloop/components/app.rb +0 -12
  74. data/examples/chat-app/app/hyperloop/components/formatted_div.rb +0 -15
  75. data/examples/chat-app/app/hyperloop/components/input_box.rb +0 -26
  76. data/examples/chat-app/app/hyperloop/components/message.rb +0 -29
  77. data/examples/chat-app/app/hyperloop/components/messages.rb +0 -8
  78. data/examples/chat-app/app/hyperloop/components/nav.rb +0 -30
  79. data/examples/chat-app/app/hyperloop/models/application_record.rb +0 -3
  80. data/examples/chat-app/app/hyperloop/models/message.rb +0 -6
  81. data/examples/chat-app/app/hyperloop/operations/operations.rb +0 -13
  82. data/examples/chat-app/app/hyperloop/stores/message_store.rb +0 -17
  83. data/examples/chat-app/app/policies/application_policy.rb +0 -9
  84. data/examples/chat-app/app/views/layouts/application.html.erb +0 -12
  85. data/examples/chat-app/bin/bundle +0 -3
  86. data/examples/chat-app/bin/rails +0 -9
  87. data/examples/chat-app/bin/rake +0 -9
  88. data/examples/chat-app/bin/setup +0 -34
  89. data/examples/chat-app/bin/spring +0 -17
  90. data/examples/chat-app/bin/update +0 -29
  91. data/examples/chat-app/config.ru +0 -5
  92. data/examples/chat-app/config/application.rb +0 -12
  93. data/examples/chat-app/config/boot.rb +0 -3
  94. data/examples/chat-app/config/cable.yml +0 -9
  95. data/examples/chat-app/config/database.yml +0 -25
  96. data/examples/chat-app/config/environment.rb +0 -5
  97. data/examples/chat-app/config/environments/development.rb +0 -56
  98. data/examples/chat-app/config/environments/production.rb +0 -86
  99. data/examples/chat-app/config/environments/test.rb +0 -42
  100. data/examples/chat-app/config/initializers/application_controller_renderer.rb +0 -6
  101. data/examples/chat-app/config/initializers/assets.rb +0 -11
  102. data/examples/chat-app/config/initializers/backtrace_silencers.rb +0 -7
  103. data/examples/chat-app/config/initializers/cookies_serializer.rb +0 -5
  104. data/examples/chat-app/config/initializers/filter_parameter_logging.rb +0 -4
  105. data/examples/chat-app/config/initializers/hyperloop.rb +0 -6
  106. data/examples/chat-app/config/initializers/inflections.rb +0 -16
  107. data/examples/chat-app/config/initializers/mime_types.rb +0 -4
  108. data/examples/chat-app/config/initializers/new_framework_defaults.rb +0 -24
  109. data/examples/chat-app/config/initializers/session_store.rb +0 -3
  110. data/examples/chat-app/config/initializers/wrap_parameters.rb +0 -14
  111. data/examples/chat-app/config/locales/en.yml +0 -23
  112. data/examples/chat-app/config/puma.rb +0 -47
  113. data/examples/chat-app/config/routes.rb +0 -5
  114. data/examples/chat-app/config/secrets.yml +0 -22
  115. data/examples/chat-app/config/spring.rb +0 -6
  116. data/examples/chat-app/db/migrate/20170319194429_create_message.rb +0 -9
  117. data/examples/chat-app/db/schema.rb +0 -48
  118. data/examples/chat-app/db/seeds.rb +0 -7
  119. data/examples/chat-app/lib/assets/.keep +0 -0
  120. data/examples/chat-app/lib/tasks/.keep +0 -0
  121. data/examples/chat-app/log/.keep +0 -0
  122. data/examples/chat-app/public/404.html +0 -67
  123. data/examples/chat-app/public/422.html +0 -67
  124. data/examples/chat-app/public/500.html +0 -66
  125. data/examples/chat-app/public/apple-touch-icon-precomposed.png +0 -0
  126. data/examples/chat-app/public/apple-touch-icon.png +0 -0
  127. data/examples/chat-app/public/favicon.ico +0 -0
  128. data/examples/chat-app/public/robots.txt +0 -5
  129. data/examples/chat-app/test/controllers/.keep +0 -0
  130. data/examples/chat-app/test/fixtures/.keep +0 -0
  131. data/examples/chat-app/test/fixtures/files/.keep +0 -0
  132. data/examples/chat-app/test/helpers/.keep +0 -0
  133. data/examples/chat-app/test/integration/.keep +0 -0
  134. data/examples/chat-app/test/mailers/.keep +0 -0
  135. data/examples/chat-app/test/models/.keep +0 -0
  136. data/examples/chat-app/test/test_helper.rb +0 -10
  137. data/examples/chat-app/tmp/.keep +0 -0
  138. data/examples/chat-app/vendor/assets/javascripts/.keep +0 -0
  139. data/examples/chat-app/vendor/assets/stylesheets/.keep +0 -0
  140. data/lib/hyperloop/model/version.rb +0 -5
@@ -0,0 +1,13 @@
1
+ module ReactiveRecord
2
+
3
+ module Pry
4
+
5
+ def self.rescued(e)
6
+ if defined?(PryRescue) && e.instance_variable_defined?(:@rescue_bindings) && !e.is_a?(Hyperloop::AccessViolation)
7
+ ::Pry::rescued(e)
8
+ end
9
+ end
10
+
11
+ end
12
+
13
+ end
@@ -0,0 +1,18 @@
1
+ # class ActiveRecord::Base
2
+ #
3
+ # def self.to_sync(scope_name, opts={}, &block)
4
+ # watch_list = if opts[:watch]
5
+ # [*opts.delete[:watch]]
6
+ # else
7
+ # [self]
8
+ # end
9
+ # if RUBY_ENGINE=='opal'
10
+ # watch_list.each do |klass_to_watch|
11
+ # ReactiveRecord::Base.sync_blocks[klass_to_watch][self][scope_name] << block
12
+ # end
13
+ # else
14
+ # # this is where we put server side watchers in place to sync all clients!
15
+ # end
16
+ # end
17
+ #
18
+ # end
@@ -0,0 +1,121 @@
1
+ module ReactiveRecord
2
+ # Keeps track of the details (client side) of a scope.
3
+ # The main point is to provide knowledge of what models
4
+ # the scope is joined with, and the client side
5
+ # filter proc
6
+ class ScopeDescription
7
+ def initialize(model, name, opts)
8
+ sself = self
9
+ @filter_proc = filter_proc(opts)
10
+ @name = name
11
+ model.singleton_class.send(:define_method, "_#{@name}_synchromesh_scope_description_") do
12
+ sself
13
+ end
14
+ @model = model
15
+ build_joins opts[:joins]
16
+ end
17
+
18
+ attr_reader :name
19
+
20
+ def self.find(target_model, name)
21
+ name = name.gsub(/!$/, '')
22
+ target_model.send "_#{name}_synchromesh_scope_description_"
23
+ rescue
24
+ nil
25
+ end
26
+
27
+ def filter?
28
+ @filter_proc.respond_to?(:call)
29
+ end
30
+
31
+ def collector?
32
+ @is_collector
33
+ end
34
+
35
+ def joins_with?(record)
36
+ @joins.detect do |klass, vector|
37
+ # added klass < record.class to handle STI case... should check to see if this could ever
38
+ # cause a problem. Probably not a problem.
39
+ next unless vector.any?
40
+ (klass == :all || record.class == klass || record.class < klass || klass < record.class)
41
+ end
42
+ end
43
+
44
+ def get_joins(klass)
45
+ joins = @joins[klass] if @joins.key? klass
46
+ joins ||= @joins[klass.base_class] if @joins.key?(klass.base_class)
47
+ joins || @joins[:all]
48
+ end
49
+
50
+ def related_records_for(record)
51
+ ReactiveRecord::Base.catch_db_requests([]) do
52
+ get_joins(record.class).collect do |vector|
53
+ crawl(record, *vector)
54
+ end.flatten.compact
55
+ end
56
+ end
57
+
58
+ def filter_records(related_records, args)
59
+ if collector?
60
+ Set.new(related_records.to_a.instance_exec(*args, &@filter_proc))
61
+ else
62
+ Set.new(related_records.select { |r| r.instance_exec(*args, &@filter_proc) })
63
+ end
64
+ end
65
+
66
+ # private methods
67
+
68
+ def filter_proc(opts)
69
+ return true unless opts.key?(:client) || opts.key?(:select)
70
+ client_opt = opts[:client] || opts[:select]
71
+ @is_collector = opts.key?(:select)
72
+ return client_opt if !client_opt || client_opt.respond_to?(:call)
73
+ raise 'Scope option :client or :select must be a proc, false, or nil'
74
+ end
75
+
76
+ def build_joins(joins_list)
77
+ if !@filter_proc || joins_list == []
78
+ @joins = { all: [] }
79
+ elsif joins_list.nil?
80
+ klass = @model < ActiveRecord::Base ? @model.base_class : @model
81
+ @joins = { klass => [[]], all: [] }
82
+ elsif joins_list == :all
83
+ @joins = { all: [[]] }
84
+ else
85
+ joins_list = [joins_list] unless joins_list.is_a? Array
86
+ map_joins_path joins_list
87
+ end
88
+ end
89
+
90
+ def map_joins_path(paths)
91
+ @joins = Hash.new { |h, k| h[k] = Array.new }.merge(@model => [[]])
92
+ paths.each do |path|
93
+ vector = []
94
+ path.split('.').inject(@model) do |model, attribute|
95
+ association = model.reflect_on_association(attribute)
96
+ raise build_error(path, model, attribute) unless association
97
+ vector = [association.inverse_of, *vector]
98
+ @joins[association.klass] << vector
99
+ association.klass
100
+ end
101
+ end
102
+ end
103
+
104
+ def build_error(path, model, attribute)
105
+ "Could not find joins association '#{model.name}.#{attribute}' "\
106
+ "for '#{path}' while processing scope #{@model.name}.#{@name}."
107
+ end
108
+
109
+ def crawl(item, method = nil, *vector)
110
+ if !method && item.is_a?(Collection)
111
+ item.all
112
+ elsif !method
113
+ item
114
+ elsif item.respond_to? :collect
115
+ item.collect { |record| crawl(record.send(method), *vector) }
116
+ else
117
+ crawl(item.send(method), *vector)
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,7 @@
1
+ ActiveRecord::Base.send(:define_method, :react_serializer) do
2
+ serializable_hash.merge(ReactiveRecord::Base.get_type_hash(self))
3
+ end
4
+
5
+ ActiveRecord::Relation.send(:define_method, :react_serializer) do
6
+ all.to_a.react_serializer
7
+ end
@@ -0,0 +1,478 @@
1
+ require 'set'
2
+ module ReactiveRecord
3
+
4
+ # requested cache items I think is there just so prerendering with multiple components works.
5
+ # because we have to dump the cache after each component render (during prererender) but
6
+ # we want to keep the larger cache alive (is this important???) we keep track of what got added
7
+ # to the cache during this cycle
8
+
9
+ # the point is to collect up a all records needed, with whatever attributes were required + primary key, and inheritance column
10
+ # or get all scope arrays, with the record ids
11
+
12
+ # the incoming vector includes the terminal method
13
+
14
+ # output is a hash tree of the form
15
+ # tree ::= {method => tree | [value]} | method's value is either a nested tree or a single value which is wrapped in array
16
+ # {:id => primary_key_id_value} | if its the id method we leave the array off because we know it must be an int
17
+ # {integer => tree} for collections, each item retrieved will be represented by its id
18
+ #
19
+ # example
20
+ # {
21
+ # "User" => {
22
+ # ["find", 12] => {
23
+ # :id => 12
24
+ # "email" => ["mitch@catprint.com"]
25
+ # "todos" => {
26
+ # "active" => {
27
+ # 123 =>
28
+ # {
29
+ # id: 123,
30
+ # title: ["get fetch_records_from_db done"]
31
+ # },
32
+ # 119 =>
33
+ # {
34
+ # id: 119
35
+ # title: ["go for a swim"]
36
+ # }
37
+ # ]
38
+ # }
39
+ # }
40
+ # }
41
+ # }
42
+ # }
43
+ # }
44
+
45
+ # To build this tree we first fill values for each individual vector, saving all the intermediate data
46
+ # when all these are built we build the above hash structure
47
+
48
+ # basic
49
+ # [Todo, [find, 123], title]
50
+ # -> [[Todo, [find, 123], title], "get fetch_records_from_db done", 123]
51
+
52
+ # [User, [find_by_email, "mitch@catprint.com"], first_name]
53
+ # -> [[User, [find_by_email, "mitch@catprint.com"], first_name], "Mitch", 12]
54
+
55
+ # misses
56
+ # [User, [find_by_email, "foobar@catprint.com"], first_name]
57
+ # nothing is found so nothing is downloaded
58
+ # prerendering may do this
59
+ # [User, [find_by_email, "foobar@catprint.com"]]
60
+ # which will return a cache object whose id is nil, and value is nil
61
+
62
+ # scoped collection
63
+ # [User, [find, 12], todos, active, *, title]
64
+ # -> [[User, [find, 12], todos, active, *, title], "get fetch_records_from_db done", 12, 123]
65
+ # -> [[User, [find, 12], todos, active, *, title], "go for a swim", 12, 119]
66
+
67
+ # collection with nested belongs_to
68
+ # [User, [find, 12], todos, *, team]
69
+ # -> [[User, [find, 12], todos, *, team, name], "developers", 12, 123, 252]
70
+ # [[User, [find, 12], todos, *, team, name], nil, 12, 119] <- no team defined for todo<119> so list ends early
71
+
72
+ # collections that are empty will deliver nothing
73
+ # [User, [find, 13], todos, *, team, name] # no todos for user 13
74
+ # evaluation will get this far: [[User, [find, 13], todos], nil, 13]
75
+ # nothing will match [User, [find, 13], todos, team, name] so nothing will be downloaded
76
+
77
+
78
+ # aggregate
79
+ # [User, [find, 12], address, zip_code]
80
+ # -> [[User, [find, 12], address, zip_code]], "14622", 12] <- note parent id is returned
81
+
82
+ # aggregate with a belongs_to
83
+ # [User, [find, 12], address, country, country_code]
84
+ # -> [[User, [find, 12], address, country, country_code], "US", 12, 342]
85
+
86
+ # collection * (for iterators etc)
87
+ # [User, [find, 12], todos, overdue, *all]
88
+ # -> [[User, [find, 12], todos, active, *all], [119, 123], 12]
89
+
90
+ # [Todo, [find, 119], owner, todos, active, *all]
91
+ # -> [[Todo, [find, 119], owner, todos, active, *all], [119, 123], 119, 12]
92
+
93
+ class ServerDataCache
94
+
95
+ def initialize(acting_user, preloaded_records)
96
+ @acting_user = acting_user
97
+ @cache = []
98
+ @cache_reps = {}
99
+ @requested_cache_items = Set.new
100
+ @preloaded_records = preloaded_records
101
+ end
102
+
103
+ attr_reader :cache
104
+ attr_reader :cache_reps
105
+ attr_reader :requested_cache_items
106
+
107
+ def add_item_to_cache(item)
108
+ cache << item
109
+ cache_reps[item.vector] = item
110
+ requested_cache_items << item
111
+ end
112
+
113
+ if RUBY_ENGINE != 'opal'
114
+
115
+ def self.get_model(str)
116
+ # We don't want to open a security hole by allowing some client side string to
117
+ # autoload a class, which would happen if we did a simple str.constantize.
118
+ #
119
+ # Because all AR models are loaded at boot time on the server to define the
120
+ # ActiveRecord::Base.public_columns_hash method any model which the client has
121
+ # access to should already be loaded.
122
+ #
123
+ # If str is not already loaded then we have an access violation.
124
+ unless const_defined? str
125
+ Hyperloop::InternalPolicy.raise_operation_access_violation(:undefined_const, "#{str} is not a loaded constant")
126
+ end
127
+ str.constantize
128
+ end
129
+
130
+ def [](*vector)
131
+ timing('building cache_items') do
132
+ root = CacheItem.new(self, @acting_user, vector[0], @preloaded_records)
133
+ vector[1..-1].inject(root) { |cache_item, method| cache_item.apply_method method if cache_item }
134
+ final = vector[1..-1].inject(root) { |cache_item, method| cache_item.apply_method method if cache_item }
135
+ next final unless final && final.value.respond_to?(:superclass) && final.value.superclass <= ActiveRecord::Base
136
+ Hyperloop::InternalPolicy.raise_operation_access_violation(:invalid_vector, "attempt to insecurely access relationship #{vector.last}.")
137
+ end
138
+ end
139
+
140
+ def start_timing(&block)
141
+ ServerDataCache.start_timing(&block)
142
+ end
143
+
144
+ def timing(tag, &block)
145
+ ServerDataCache.timing(tag, &block)
146
+ end
147
+
148
+ def self.start_timing(&block)
149
+ @timings = Hash.new { |h, k| h[k] = 0 }
150
+ start_time = Time.now
151
+ yield.tap do
152
+ ::Rails.logger.debug "********* Total Time #{total = Time.now - start_time} ***********************"
153
+ sum = 0
154
+ @timings.sort_by(&:last).reverse.each do |tag, time|
155
+ ::Rails.logger.debug " #{tag}: #{time} (#{(time/total*100).to_i})%"
156
+ sum += time
157
+ end
158
+ ::Rails.logger.debug "********* Other Time ***********************"
159
+ end
160
+ end
161
+
162
+ def self.timing(tag, &block)
163
+ start_time = Time.now
164
+ tag = tag.to_sym
165
+ yield.tap { @timings[tag] += (Time.now - start_time) if @timings }
166
+ end
167
+
168
+ def self.[](models, associations, vectors, acting_user)
169
+ start_timing do
170
+ timing(:public_columns_hash) { ActiveRecord::Base.public_columns_hash }
171
+ result = nil
172
+ ActiveRecord::Base.transaction do
173
+ cache = new(acting_user, timing(:save_records) { ReactiveRecord::Base.save_records(models, associations, acting_user, false, false) })
174
+ timing(:process_vectors) { vectors.each { |vector| cache[*vector] } }
175
+ timing(:as_json) { result = cache.as_json }
176
+ raise ActiveRecord::Rollback, "This Rollback is intentional!"
177
+ end
178
+ result
179
+ end
180
+ end
181
+
182
+ def clear_requests
183
+ @requested_cache_items = Set.new
184
+ end
185
+
186
+ def as_json
187
+ @requested_cache_items.inject({}) do |hash, cache_item|
188
+ hash.deep_merge! cache_item.as_hash
189
+ end
190
+ end
191
+
192
+ def select(&block); @cache.select(&block); end
193
+
194
+ def detect(&block); @cache.detect(&block); end
195
+
196
+ def inject(initial, &block); @cache.inject(initial) &block; end
197
+
198
+ class CacheItem
199
+
200
+ attr_reader :vector
201
+ attr_reader :absolute_vector
202
+ attr_reader :root
203
+ attr_reader :acting_user
204
+
205
+ def value
206
+ @value # which is a ActiveRecord object
207
+ end
208
+
209
+ def method
210
+ @vector.last
211
+ end
212
+
213
+ def self.new(db_cache, acting_user, klass, preloaded_records)
214
+ klass = ServerDataCache.get_model(klass)
215
+ if existing = ServerDataCache.timing(:root_lookup) { db_cache.cache.detect { |cached_item| cached_item.vector == [klass] } }
216
+ return existing
217
+ end
218
+ super
219
+ end
220
+
221
+ def initialize(db_cache, acting_user, klass, preloaded_records)
222
+ @db_cache = db_cache
223
+ @acting_user = acting_user
224
+ @vector = @absolute_vector = [klass]
225
+ @value = klass
226
+ @parent = nil
227
+ @root = self
228
+ @preloaded_records = preloaded_records
229
+ @db_cache.add_item_to_cache self
230
+ end
231
+
232
+ def start_timing(&block)
233
+ ServerDataCache.class.start_timing(&block)
234
+ end
235
+
236
+ def timing(tag, &block)
237
+ ServerDataCache.timing(tag, &block)
238
+ end
239
+
240
+ def apply_method_to_cache(method)
241
+ @db_cache.cache.inject(nil) do |representative, cache_item|
242
+ if cache_item.vector == vector
243
+ if method == "*"
244
+ # apply_star does the security check if value is present
245
+ cache_item.apply_star || representative
246
+ elsif method == "*all"
247
+ # if we secure the collection then we assume its okay to read the ids
248
+ secured_value = cache_item.value.__secure_collection_check(@acting_user)
249
+ cache_item.build_new_cache_item(timing(:active_record) { secured_value.collect { |record| record.id } }, method, method)
250
+ elsif method == "*count"
251
+ secured_value = cache_item.value.__secure_collection_check(@acting_user)
252
+ cache_item.build_new_cache_item(timing(:active_record) { cache_item.value.__secure_collection_check(@acting_user).count }, method, method)
253
+ elsif preloaded_value = @preloaded_records[cache_item.absolute_vector + [method]]
254
+ # no security check needed since we already evaluated this
255
+ cache_item.build_new_cache_item(preloaded_value, method, method)
256
+ elsif aggregation = cache_item.aggregation?(method)
257
+ # aggregations are not protected
258
+ cache_item.build_new_cache_item(aggregation.mapping.collect { |attribute, accessor| cache_item.value[attribute] }, method, method)
259
+ else
260
+ if !cache_item.value || cache_item.value.is_a?(Array)
261
+ # seeing as we just returning representative, no check is needed (its already checked)
262
+ representative
263
+ else
264
+ begin
265
+ secured_method = "__secure_remote_access_to_#{[*method].first}"
266
+
267
+ # order is important. This check must be first since scopes can have same name as attributes!
268
+ if cache_item.value.respond_to? secured_method
269
+ cache_item.build_new_cache_item(timing(:active_record) { cache_item.value.send(secured_method, cache_item.value, @acting_user, *([*method][1..-1])) }, method, method)
270
+ elsif (cache_item.value.class < ActiveRecord::Base) && cache_item.value.attributes.has_key?(method) # TODO: second check is not needed, its built into check_permmissions, check should be does class respond to check_permissions...
271
+ cache_item.value.check_permission_with_acting_user(@acting_user, :view_permitted?, method)
272
+ cache_item.build_new_cache_item(timing(:active_record) { cache_item.value.send(*method) }, method, method)
273
+ else
274
+ raise "method missing"
275
+ end
276
+ rescue Exception => e # this check may no longer be needed as we are quite explicit now on which methods we apply
277
+ # ReactiveRecord::Pry::rescued(e)
278
+ ::Rails.logger.debug "\033[0;31;1mERROR: HyperModel exception caught when applying #{method} to db object #{cache_item.value}: #{e}\033[0;30;21m"
279
+ raise e, "HyperModel fetching records failed, exception caught when applying #{method} to db object #{cache_item.value}: #{e}", e.backtrace
280
+ end
281
+ end
282
+ end
283
+ else
284
+ representative
285
+ end
286
+ end
287
+ end
288
+
289
+ def aggregation?(method)
290
+ if method.is_a?(String) && @value.class.respond_to?(:reflect_on_aggregation)
291
+ aggregation = @value.class.reflect_on_aggregation(method.to_sym)
292
+ if aggregation && !(aggregation.klass < ActiveRecord::Base) && @value.send(method)
293
+ aggregation
294
+ end
295
+ end
296
+ end
297
+
298
+ def apply_star
299
+ if @value && @value.__secure_collection_check(@acting_user) && @value.length > 0
300
+ i = -1
301
+ @value.inject(nil) do |representative, current_value|
302
+ i += 1
303
+ if preloaded_value = @preloaded_records[@absolute_vector + ["*#{i}"]]
304
+ build_new_cache_item(preloaded_value, "*", "*#{i}")
305
+ else
306
+ build_new_cache_item(current_value, "*", "*#{i}")
307
+ end
308
+ end
309
+ else
310
+ build_new_cache_item([], "*", "*")
311
+ end
312
+ end
313
+
314
+ # TODO replace instance_eval with a method like clone_new_child(....)
315
+ def build_new_cache_item(new_value, method, absolute_method)
316
+ new_parent = self
317
+ self.clone.instance_eval do
318
+ @vector = @vector + [method] # don't push it on since you need a new vector!
319
+ @absolute_vector = @absolute_vector + [absolute_method]
320
+ @value = new_value
321
+ @db_cache.add_item_to_cache self
322
+ @parent = new_parent
323
+ @root = new_parent.root
324
+ self
325
+ end
326
+ end
327
+
328
+ def apply_method(method)
329
+ if method.is_a? Array and method.first == "find_by_id"
330
+ method[0] = "find"
331
+ elsif method.is_a? String and method =~ /^\*[0-9]+$/
332
+ method = "*"
333
+ end
334
+ new_vector = vector + [method]
335
+ timing('apply_method lookup') { @db_cache.cache_reps[new_vector] } || apply_method_to_cache(method)
336
+ end
337
+
338
+ def jsonize(method)
339
+ # sadly standard json converts {[:foo, nil] => 123} to {"['foo', nil]": 123}
340
+ # luckily [:foo, nil] does convert correctly
341
+ # so we check the methods and force proper conversion
342
+ method.is_a?(Array) ? method.to_json : method
343
+ end
344
+
345
+ def merge_inheritance_column(children)
346
+ if @value.attributes.key? @value.class.inheritance_column
347
+ children[@value.class.inheritance_column] = [@value[@value.class.inheritance_column]]
348
+ end
349
+ children
350
+ end
351
+
352
+ def as_hash(children = nil)
353
+ unless children
354
+ return {} if @value.is_a?(Class) && (@value < ActiveRecord::Base)
355
+ children = [@value.is_a?(BigDecimal) ? @value.to_f : @value]
356
+ end
357
+ if @parent
358
+ if method == "*"
359
+ if @value.is_a? Array # this happens when a scope is empty there is test case, but
360
+ @parent.as_hash({}) # does it work for all edge cases?
361
+ else
362
+ @parent.as_hash({@value.id => children})
363
+ end
364
+ elsif (@value.class < ActiveRecord::Base) && children.is_a?(Hash)
365
+ id = method.is_a?(Array) && method.first == "new" ? [nil] : [@value.id]
366
+ # c = children.merge(id: id)
367
+ # if @value.attributes.key? @value.class.inheritance_column
368
+ # c[@value.class.inheritance_column] = [@value[@value.class.inheritance_column]]
369
+ # end
370
+ @parent.as_hash(jsonize(method) => merge_inheritance_column(children.merge(id: id)))
371
+ elsif method == '*all'
372
+ @parent.as_hash('*all' => children.first)
373
+ else
374
+ @parent.as_hash(jsonize(method) => children)
375
+ end
376
+ else
377
+ { method.name => children }
378
+ end
379
+ end
380
+
381
+ def to_json
382
+ @value.to_json
383
+ end
384
+
385
+ end
386
+
387
+ end
388
+
389
+ =begin
390
+ tree is a hash, target is the object that will be filled in with the data hanging off the key.
391
+ first time around target == nil, so for each key, value pair we do this: load_from_json(value, Object.const_get(JSON.parse(key)))
392
+ keys:
393
+ ':*all': target.replace tree["*all"].collect { |id| target.proxy_association.klass.find(id) }
394
+ Example: {'*all': [1, 7, 19, 23]} target is a collection and will now have 4 records: 1, 7, 19, 23
395
+
396
+ 'id': if value is an array then target.id = value.first
397
+ Example: {'id': [17]} Example the target is a record, and its id is now set to 17
398
+
399
+ '*count': target.set_count_state(value.first) note: set_count_state sets the count of a collection and updates the associated state variable
400
+ integer-like-string-or-number: target.push_and_update_belongs_to(key) note: collection will be a has_many association, so we are doing a target << find(key), and updating both ends of the relationship
401
+ [:new, nnn] do a ReactiveRecord::Base.find_by_object_id(target.base_class, method[1]) and that becomes the new target, with val being passed allow_change
402
+ [...] and current target is NOT an ActiveRecord Model (??? a collection ???) then send key to target, and that becomes new target
403
+ but note if value is an array then the scope returned nil, so we destroy the bogus record, and set new target back to nil
404
+ new_target.destroy and new_target = nil if value.is_a? Array
405
+ [...] and current target IS AN ActiveRecord Model (not a collection) then target.backing_record.update_attribute([method], target.backing_record.convert(method, value.first))
406
+ aggregation:
407
+ target.class.respond_to?(:reflect_on_aggregation) and aggregation = target.class.reflect_on_aggregation(method) and !(aggregation.klass < ActiveRecord::Base)
408
+ target.send "#{method}=", aggregation.deserialize(value.first)
409
+ other-string-method-name:
410
+ if value is a an array then value.first is the new value and we do target.send "{key}=", value.first
411
+ if value is a hash
412
+ =end
413
+
414
+
415
+ def self.load_from_json(tree, target = nil)
416
+
417
+ # have to process *all before any other items
418
+ # we leave the "*all" key in just for debugging purposes, and then skip it below
419
+
420
+ if sorted_collection = tree["*all"]
421
+ target.replace sorted_collection.collect { |id| target.proxy_association.klass.find(id) }
422
+ end
423
+
424
+ if id_value = tree["id"] and id_value.is_a? Array
425
+ target.id = id_value.first
426
+ end
427
+ tree.each do |method, value|
428
+ method = JSON.parse(method) rescue method
429
+ new_target = nil
430
+
431
+ if method == "*all"
432
+ next # its already been processed above
433
+ elsif !target
434
+ load_from_json(value, Object.const_get(method))
435
+ elsif method == "*count"
436
+ target.set_count_state(value.first)
437
+ elsif method.is_a? Integer or method =~ /^[0-9]+$/
438
+ new_target = target.push_and_update_belongs_to(method)
439
+ #target << (new_target = target.proxy_association.klass.find(method))
440
+ elsif method.is_a? Array
441
+ if method[0] == "new"
442
+ new_target = ReactiveRecord::Base.lookup_by_object_id(method[1])
443
+ elsif !(target.class < ActiveRecord::Base)
444
+ new_target = target.send(*method)
445
+ # value is an array if scope returns nil, so we destroy the bogus record
446
+ new_target.destroy and new_target = nil if value.is_a? Array
447
+ else
448
+ target.backing_record.update_simple_attribute([method], target.backing_record.convert(method, value.first))
449
+ end
450
+ elsif target.class.respond_to?(:reflect_on_aggregation) &&
451
+ (aggregation = target.class.reflect_on_aggregation(method)) &&
452
+ !(aggregation.klass < ActiveRecord::Base)
453
+ value = [aggregation.deserialize(value.first)] unless value.first.is_a?(aggregation.klass)
454
+
455
+ target.send "#{method}=", value.first
456
+ elsif value.is_a? Array
457
+ # we cannot use target.send "#{method}=" here because it might be a server method, which does not have a setter
458
+ # a better fix might be something like target._internal_attribute_hash[method] = ...
459
+ target.backing_record.set_attr_value(method, value.first) unless method == :id
460
+ elsif value.is_a? Hash and value[:id] and value[:id].first and association = target.class.reflect_on_association(method)
461
+ # not sure if its necessary to check the id above... is it possible to for the method to be an association but not have an id?
462
+ new_target = association.klass.find(value[:id].first)
463
+ target.send "#{method}=", new_target
464
+ elsif !(target.class < ActiveRecord::Base)
465
+ new_target = target.send(*method)
466
+ # value is an array if scope returns nil, so we destroy the bogus record
467
+ new_target.destroy and new_target = nil if value.is_a? Array
468
+ else
469
+ new_target = target.send("#{method}=", target.send(method))
470
+ end
471
+ load_from_json(value, new_target) if new_target
472
+ end
473
+ rescue Exception => e
474
+ # debugger
475
+ raise e
476
+ end
477
+ end
478
+ end