hyper-model 0.6.0 → 0.99.0

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